1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
//! Crash math: the curve, the payout floor, and the house edge.
use std::time::Duration;
use zagan_bet::crash::{
crash_point_from_uniform, multiplier_at, multiplier_at_secs, payout, time_for_multiplier,
HOUSE_EDGE, MAX_MULTIPLIER,
};
fn close(a: f64, b: f64, tolerance: f64) {
assert!(
(a - b).abs() <= tolerance,
"expected {b} +/- {tolerance}, got {a}"
);
}
#[test]
fn the_multiplier_doubles_every_five_seconds() {
close(multiplier_at(Duration::ZERO), 1.0, 1e-9);
close(multiplier_at(Duration::from_secs(5)), 2.0, 1e-9);
close(multiplier_at(Duration::from_secs(10)), 4.0, 1e-6);
close(multiplier_at(Duration::from_millis(2500)), 1.414_213_562, 1e-6);
// Never below 1x, even for nonsense input.
assert_eq!(multiplier_at_secs(-3.0), 1.0);
assert_eq!(multiplier_at_secs(f64::NAN), 1.0);
assert_eq!(multiplier_at_secs(10_000.0), MAX_MULTIPLIER);
}
#[test]
fn the_time_for_a_multiplier_is_the_inverse_of_the_curve() {
close(time_for_multiplier(2.0).as_secs_f64(), 5.0, 1e-9);
close(time_for_multiplier(10.0).as_secs_f64(), 16.609_640, 1e-4);
close(multiplier_at(time_for_multiplier(3.5)), 3.5, 1e-9);
}
#[test]
fn cashout_value_is_floored_to_whole_pln() {
assert_eq!(payout(10, 1.0), 10);
assert_eq!(payout(10, 1.05), 10, "half a PLN is not paid out");
assert_eq!(payout(10, 1.10), 11);
assert_eq!(payout(10, 2.47), 24);
assert_eq!(payout(7, 3.0), 21);
assert_eq!(payout(0, 2.0), 0);
assert_eq!(payout(10, f64::NAN), 0);
assert_eq!(payout(10, f64::INFINITY), 0);
assert_eq!(
payout(u64::MAX, MAX_MULTIPLIER),
u64::MAX,
"a huge win saturates instead of wrapping"
);
}
#[test]
fn crash_points_follow_the_house_edge() {
// P(crash >= m) should be (1 - edge) / m.
let samples = 100_000;
for target in [1.5_f64, 2.0, 5.0, 50.0] {
let survivors = (0..samples)
.filter(|index| {
let u = *index as f64 / samples as f64;
crash_point_from_uniform(u) >= target
})
.count();
let observed = survivors as f64 / samples as f64;
let expected = (1.0 - HOUSE_EDGE) / target;
close(observed, expected, 0.002);
}
}
#[test]
fn an_instant_cashout_still_pays_the_house_edge() {
// One round in a hundred crashes at 1.00x, so cashing out instantly returns
// the stake 99% of the time and nothing 1% of the time: no free money.
let samples = 100_000;
let instant_crashes = (0..samples)
.filter(|index| {
let u = *index as f64 / samples as f64;
crash_point_from_uniform(u) <= 1.0
})
.count();
close(instant_crashes as f64 / samples as f64, HOUSE_EDGE, 0.001);
let stake = 10;
let kept = payout(stake, 1.0);
assert_eq!(kept, stake);
let ev = kept as f64 * (1.0 - HOUSE_EDGE);
close(ev, stake as f64 * (1.0 - HOUSE_EDGE), 1e-9);
}
#[test]
fn crash_points_are_clamped_to_the_liability_cap() {
assert_eq!(crash_point_from_uniform(0.0), 1.0);
assert_eq!(crash_point_from_uniform(-1.0), 1.0);
assert_eq!(crash_point_from_uniform(f64::NAN), 1.0);
close(crash_point_from_uniform(0.5), 1.98, 1e-9);
close(crash_point_from_uniform(0.99), 99.0, 1e-9);
assert_eq!(crash_point_from_uniform(0.999_999), MAX_MULTIPLIER);
assert_eq!(crash_point_from_uniform(1.0), MAX_MULTIPLIER);
}
|