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
|
//! Shared crash game math and wire state.
//!
//! The multiplier is a pure function of elapsed time, so the client can draw it
//! locally. The crash point is drawn up front and never leaves the server until
//! the round ends; the payout at cashout is always computed from the server's
//! clock, never from anything the client sends.
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// `exp(GROWTH_RATE * t)` doubles the multiplier every five seconds.
pub const GROWTH_RATE: f64 = 0.138_629_436_111_989_07; // ln(2) / 5
/// House edge baked into the crash-point distribution.
pub const HOUSE_EDGE: f64 = 0.01;
/// Liability cap. At the cap `P(crash >= m)` stops following `edge / m`.
pub const MAX_MULTIPLIER: f64 = 1000.0;
/// Multiplier after `elapsed` seconds of a round.
pub fn multiplier_at(elapsed: Duration) -> f64 {
multiplier_at_secs(elapsed.as_secs_f64())
}
pub fn multiplier_at_secs(seconds: f64) -> f64 {
if !seconds.is_finite() || seconds <= 0.0 {
return 1.0;
}
(GROWTH_RATE * seconds)
.exp()
.clamp(1.0, MAX_MULTIPLIER)
}
/// How long a round runs before reaching `multiplier`.
pub fn time_for_multiplier(multiplier: f64) -> Duration {
let multiplier = multiplier.clamp(1.0, MAX_MULTIPLIER);
Duration::from_secs_f64(multiplier.ln() / GROWTH_RATE)
}
/// Crash point for a uniform draw in `[0, 1)`.
///
/// `crash = (1 - edge) / (1 - u)` gives `P(crash >= m) = edge_keep / m` for
/// every `m` in `[1, MAX_MULTIPLIER]`, so a fixed-target cashout returns
/// `(1 - edge)` of the stake on average.
pub fn crash_point_from_uniform(u: f64) -> f64 {
let u = if u.is_finite() {
u.clamp(0.0, 0.999_999_999)
} else {
0.0
};
((1.0 - HOUSE_EDGE) / (1.0 - u)).clamp(1.0, MAX_MULTIPLIER)
}
/// Whole-PLN cashout value. Balances are integers, so the payout is floored.
pub fn payout(bet: u64, multiplier: f64) -> u64 {
if bet == 0 || !multiplier.is_finite() || multiplier <= 0.0 {
return 0;
}
let value = bet as f64 * multiplier.min(MAX_MULTIPLIER);
if value >= u64::MAX as f64 {
u64::MAX
} else {
value.floor() as u64
}
}
/// Round state as sent to the client.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CrashState {
pub session_id: String,
/// `running`, `cashed`, `crashed` or `forfeit`.
pub phase: String,
pub bet: u64,
pub balance: i64,
/// Live multiplier while running, otherwise the value the round ended at.
pub multiplier: f64,
/// `floor(bet * multiplier)` for the value above.
pub potential: u64,
/// Hidden until the round ends.
pub crash_point: Option<f64>,
pub payout: u64,
pub result: String,
}
impl CrashState {
pub fn is_running(&self) -> bool {
self.phase == "running"
}
}
|