diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-29 20:43:09 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-29 20:43:09 +0100 |
| commit | 6e2787d792de64c003eff03b62a2df798ef851c0 (patch) | |
| tree | 0e9bf97b2f71df5fb9eb00d5195d32b8326886ff /src/crash.rs | |
| parent | v2.2.1: forfeit blackjack bets when leaving or disconnecting (diff) | |
| download | zagan-bet-6e2787d792de64c003eff03b62a2df798ef851c0.tar.gz zagan-bet-6e2787d792de64c003eff03b62a2df798ef851c0.zip | |
v2.3.0: add crash with local multiplier and pushed crash event
Diffstat (limited to '')
| -rw-r--r-- | src/crash.rs | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/src/crash.rs b/src/crash.rs new file mode 100644 index 0000000..55aab19 --- /dev/null +++ b/src/crash.rs | |||
| @@ -0,0 +1,87 @@ | |||
| 1 | //! Shared crash game math and wire state. | ||
| 2 | //! | ||
| 3 | //! The multiplier is a pure function of elapsed time, so the client can draw it | ||
| 4 | //! locally. The crash point is drawn up front and never leaves the server until | ||
| 5 | //! the round ends; the payout at cashout is always computed from the server's | ||
| 6 | //! clock, never from anything the client sends. | ||
| 7 | |||
| 8 | use serde::{Deserialize, Serialize}; | ||
| 9 | use std::time::Duration; | ||
| 10 | |||
| 11 | /// `exp(GROWTH_RATE * t)` doubles the multiplier every five seconds. | ||
| 12 | pub const GROWTH_RATE: f64 = 0.138_629_436_111_989_07; // ln(2) / 5 | ||
| 13 | /// House edge baked into the crash-point distribution. | ||
| 14 | pub const HOUSE_EDGE: f64 = 0.01; | ||
| 15 | /// Liability cap. At the cap `P(crash >= m)` stops following `edge / m`. | ||
| 16 | pub const MAX_MULTIPLIER: f64 = 1000.0; | ||
| 17 | |||
| 18 | /// Multiplier after `elapsed` seconds of a round. | ||
| 19 | pub fn multiplier_at(elapsed: Duration) -> f64 { | ||
| 20 | multiplier_at_secs(elapsed.as_secs_f64()) | ||
| 21 | } | ||
| 22 | |||
| 23 | pub fn multiplier_at_secs(seconds: f64) -> f64 { | ||
| 24 | if !seconds.is_finite() || seconds <= 0.0 { | ||
| 25 | return 1.0; | ||
| 26 | } | ||
| 27 | (GROWTH_RATE * seconds) | ||
| 28 | .exp() | ||
| 29 | .clamp(1.0, MAX_MULTIPLIER) | ||
| 30 | } | ||
| 31 | |||
| 32 | /// How long a round runs before reaching `multiplier`. | ||
| 33 | pub fn time_for_multiplier(multiplier: f64) -> Duration { | ||
| 34 | let multiplier = multiplier.clamp(1.0, MAX_MULTIPLIER); | ||
| 35 | Duration::from_secs_f64(multiplier.ln() / GROWTH_RATE) | ||
| 36 | } | ||
| 37 | |||
| 38 | /// Crash point for a uniform draw in `[0, 1)`. | ||
| 39 | /// | ||
| 40 | /// `crash = (1 - edge) / (1 - u)` gives `P(crash >= m) = edge_keep / m` for | ||
| 41 | /// every `m` in `[1, MAX_MULTIPLIER]`, so a fixed-target cashout returns | ||
| 42 | /// `(1 - edge)` of the stake on average. | ||
| 43 | pub fn crash_point_from_uniform(u: f64) -> f64 { | ||
| 44 | let u = if u.is_finite() { | ||
| 45 | u.clamp(0.0, 0.999_999_999) | ||
| 46 | } else { | ||
| 47 | 0.0 | ||
| 48 | }; | ||
| 49 | ((1.0 - HOUSE_EDGE) / (1.0 - u)).clamp(1.0, MAX_MULTIPLIER) | ||
| 50 | } | ||
| 51 | |||
| 52 | /// Whole-PLN cashout value. Balances are integers, so the payout is floored. | ||
| 53 | pub fn payout(bet: u64, multiplier: f64) -> u64 { | ||
| 54 | if bet == 0 || !multiplier.is_finite() || multiplier <= 0.0 { | ||
| 55 | return 0; | ||
| 56 | } | ||
| 57 | let value = bet as f64 * multiplier.min(MAX_MULTIPLIER); | ||
| 58 | if value >= u64::MAX as f64 { | ||
| 59 | u64::MAX | ||
| 60 | } else { | ||
| 61 | value.floor() as u64 | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | /// Round state as sent to the client. | ||
| 66 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||
| 67 | pub struct CrashState { | ||
| 68 | pub session_id: String, | ||
| 69 | /// `running`, `cashed`, `crashed` or `forfeit`. | ||
| 70 | pub phase: String, | ||
| 71 | pub bet: u64, | ||
| 72 | pub balance: i64, | ||
| 73 | /// Live multiplier while running, otherwise the value the round ended at. | ||
| 74 | pub multiplier: f64, | ||
| 75 | /// `floor(bet * multiplier)` for the value above. | ||
| 76 | pub potential: u64, | ||
| 77 | /// Hidden until the round ends. | ||
| 78 | pub crash_point: Option<f64>, | ||
| 79 | pub payout: u64, | ||
| 80 | pub result: String, | ||
| 81 | } | ||
| 82 | |||
| 83 | impl CrashState { | ||
| 84 | pub fn is_running(&self) -> bool { | ||
| 85 | self.phase == "running" | ||
| 86 | } | ||
| 87 | } | ||
