From 6e2787d792de64c003eff03b62a2df798ef851c0 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 20:43:09 +0100 Subject: v2.3.0: add crash with local multiplier and pushed crash event --- src/crash.rs | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/crash.rs (limited to 'src/crash.rs') 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 @@ +//! 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, + pub payout: u64, + pub result: String, +} + +impl CrashState { + pub fn is_running(&self) -> bool { + self.phase == "running" + } +} -- cgit v1.2.3