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/server/games/crash.rs | 216 +++++++++++++++++++++++++++++++++++++++++++ src/server/games/mod.rs | 1 + src/server/games/registry.rs | 2 + 3 files changed, 219 insertions(+) create mode 100644 src/server/games/crash.rs (limited to 'src/server/games') diff --git a/src/server/games/crash.rs b/src/server/games/crash.rs new file mode 100644 index 0000000..ba73c7f --- /dev/null +++ b/src/server/games/crash.rs @@ -0,0 +1,216 @@ +use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; +use crate::crash::{crash_point_from_uniform, payout, CrashState}; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +/// Monotonic seconds. Injected so tests can drive a round without sleeping. +pub trait Clock: Send + Sync { + fn now(&self) -> f64; +} + +pub struct RealClock { + origin: Instant, +} + +impl RealClock { + pub fn new() -> Self { + Self { + origin: Instant::now(), + } + } +} + +impl Clock for RealClock { + fn now(&self) -> f64 { + self.origin.elapsed().as_secs_f64() + } +} + +/// A clock a test can move by hand. Clones share the same instant. +#[derive(Clone, Default)] +pub struct ManualClock { + seconds: Arc>, +} + +impl ManualClock { + pub fn new() -> Self { + Self::default() + } + + pub fn set(&self, seconds: f64) { + *self.seconds.lock().unwrap() = seconds; + } + + pub fn advance(&self, seconds: f64) { + *self.seconds.lock().unwrap() += seconds; + } + + pub fn seconds(&self) -> f64 { + *self.seconds.lock().unwrap() + } +} + +impl Clock for ManualClock { + fn now(&self) -> f64 { + self.seconds() + } +} + +/// One crash round. Lives in the game session's `data` while it runs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CrashRound { + pub bet: u64, + /// Drawn at start, revealed only when the round ends. + pub crash_point: f64, + pub started_at: f64, + /// `running`, `cashed`, `crashed` or `forfeit`. + pub phase: String, + pub result: String, + /// Live while running; frozen at the final value afterwards. + pub multiplier: f64, + pub payout: u64, +} + +impl CrashRound { + pub fn new(bet: u64, crash_point: f64, started_at: f64) -> Self { + Self { + bet, + crash_point, + started_at, + phase: "running".to_string(), + result: "playing".to_string(), + multiplier: 1.0, + payout: 0, + } + } + + pub fn is_running(&self) -> bool { + self.phase == "running" + } + + pub fn cash_out(&mut self, multiplier: f64) -> u64 { + self.multiplier = multiplier; + self.payout = payout(self.bet, multiplier); + self.phase = "cashed".to_string(); + self.result = "cashed".to_string(); + self.payout + } + + /// The round ended before the player could cash out. + pub fn crash(&mut self) { + self.multiplier = self.crash_point; + self.payout = 0; + self.phase = "crashed".to_string(); + self.result = "crashed".to_string(); + } + + pub fn forfeit(&mut self) { + self.payout = 0; + self.phase = "forfeit".to_string(); + self.result = "forfeit".to_string(); + } + + pub fn state(&self, session_id: &str, balance: i64) -> CrashState { + CrashState { + session_id: session_id.to_string(), + phase: self.phase.clone(), + bet: self.bet, + balance, + multiplier: self.multiplier, + potential: payout(self.bet, self.multiplier), + crash_point: if self.is_running() { + None + } else { + Some(self.crash_point) + }, + payout: self.payout, + result: self.result.clone(), + } + } + + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } + + pub fn from_json(raw: &str) -> Result { + serde_json::from_str(raw) + } +} + +/// Crash-point generator. Tests queue fixed uniforms; production draws random +/// ones. The visible crash point stays hidden until `poll` or cashout ends the +/// round. +pub struct CrashGame { + uniforms: VecDeque, +} + +impl CrashGame { + pub fn new() -> Self { + Self { + uniforms: VecDeque::new(), + } + } + + pub fn with_uniform(uniform: f64) -> Self { + Self::with_uniforms([uniform]) + } + + pub fn with_uniforms(uniforms: impl IntoIterator) -> Self { + Self { + uniforms: uniforms.into_iter().collect(), + } + } + + pub fn next_crash_point(&mut self) -> f64 { + let uniform = self + .uniforms + .pop_front() + .unwrap_or_else(rand::random::); + crash_point_from_uniform(uniform) + } +} + +impl Game for CrashGame { + fn game_type(&self) -> &'static str { + "crash" + } + + fn get_name(&self) -> &'static str { + "Crash" + } + + fn get_description(&self) -> &'static str { + "Crash: cash out before the multiplier crashes." + } + + fn prepare_state(&self, _username: &str, bet_amount: u64) -> GameState { + let mut data = HashMap::new(); + data.insert("status".to_string(), "ready".to_string()); + GameState { + session_id: String::new(), + game_type: "crash".to_string(), + bet_amount, + phase: GamePhase::WaitingToStart, + data, + } + } + + fn process_action( + &self, + _state: &GameState, + _action: &str, + _params: &[&str], + ) -> Result { + Err(GameError::InvalidAction( + "Crash uses CRASH_CASHOUT and CRASH_FORFEIT".to_string(), + )) + } + + fn calculate_result(&self, _state: &GameState) -> Result { + Err(GameError::InternalError( + "Crash settles through CRASH_CASHOUT".to_string(), + )) + } +} diff --git a/src/server/games/mod.rs b/src/server/games/mod.rs index 07c5772..5de9eef 100644 --- a/src/server/games/mod.rs +++ b/src/server/games/mod.rs @@ -1,4 +1,5 @@ pub mod blackjack; +pub mod crash; pub mod registry; pub mod roulette; pub mod session; diff --git a/src/server/games/registry.rs b/src/server/games/registry.rs index 7ff8fbf..7a8b605 100644 --- a/src/server/games/registry.rs +++ b/src/server/games/registry.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] use super::blackjack::BlackjackGame; +use super::crash::CrashGame; use super::roulette::RouletteGame; use super::slots::SlotsGame; use super::traits::Game; @@ -25,6 +26,7 @@ impl GameRegistry { registry.register_game(SlotsGame::new()); registry.register_game(RouletteGame::new()); registry.register_game(BlackjackGame::new()); + registry.register_game(CrashGame::new()); registry } -- cgit v1.2.3