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