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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
//! Crash math: the curve, the payout floor, and the house edge.
use std::time::Duration;
use zagan_bet::crash::{
HOUSE_EDGE, MAX_MULTIPLIER, crash_point_from_uniform, multiplier_at, multiplier_at_secs,
payout, time_for_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_kr() {
assert_eq!(payout(10, 1.0), 10);
assert_eq!(payout(10, 1.05), 10, "half a Kr 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);
}
#[test]
fn a_seed_commits_to_its_crash_point_and_can_be_verified() {
use zagan_bet::crash::{
SEED_LEN, commit_seed, crash_point_from_seed, seed_from_hex, seed_to_uniform, verify_seed,
};
let mut seed = [0u8; SEED_LEN];
let value = (0.67_f64 * 18_446_744_073_709_551_616.0) as u64;
seed[..8].copy_from_slice(&value.to_be_bytes());
let commit = commit_seed(&seed);
let hex = format!(
"{}",
seed.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
);
assert_eq!(seed_from_hex(&hex).as_deref(), Some(seed.as_slice()));
assert!(verify_seed(&hex, &commit));
assert!(!verify_seed(&hex, &"00".repeat(32)), "a wrong commit fails");
assert!(!verify_seed("nope", &commit), "bad hex fails");
// The point the client recomputes is the point the server committed to.
close(crash_point_from_seed(&seed), 3.0, 1e-9);
close(crash_point_from_seed(&seed), 3.0, 1e-9);
// The first eight bytes are the uniform; the rest are still hashed.
close(seed_to_uniform(&seed), 0.67, 1e-9);
let mut altered = seed;
altered[SEED_LEN - 1] = 1;
assert!(!verify_seed(
&altered
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
&commit
));
}
|