summaryrefslogtreecommitdiffstats
path: root/src/crash.rs
blob: 2936b998f420cc221a73a3c1e2809965eb95974d (plain) (blame)
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
//! 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"
    }
}