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/client/games/crash.rs | 91 ++++++++++++++++++++++++++++++++++++++++++++ src/client/games/manager.rs | 90 +++++++++++++++++++++++++++++++++++++++++++ src/client/games/mod.rs | 1 + src/client/games/response.rs | 21 ++++++++++ 4 files changed, 203 insertions(+) create mode 100644 src/client/games/crash.rs (limited to 'src/client/games') diff --git a/src/client/games/crash.rs b/src/client/games/crash.rs new file mode 100644 index 0000000..19733f2 --- /dev/null +++ b/src/client/games/crash.rs @@ -0,0 +1,91 @@ +use super::traits::{Game, GameState}; + +pub const CRASH_BOARD_LINES: usize = 6; + +pub struct CrashGame; + +impl CrashGame { + pub fn new() -> Self { + Self + } + + fn value<'a>(state: &'a GameState, key: &str) -> Option<&'a str> { + state.data.get(key).map(String::as_str) + } + + fn status_text(state: &GameState) -> String { + let status = Self::value(state, "status").unwrap_or("ready"); + match status { + "starting" => "Starting...".to_string(), + "running" => "Rising... cash out before it crashes.".to_string(), + "cashed" => format!( + "Cashed out at {}x Won {} PLN", + Self::value(state, "multiplier").unwrap_or("1.00"), + Self::value(state, "payout").unwrap_or("0") + ), + "crashed" => format!( + "CRASHED at {}x Bet lost", + Self::value(state, "crash_point").unwrap_or("?") + ), + "forfeit" => "Forfeited. Bet lost.".to_string(), + _ => "Press [SPACE] to start.".to_string(), + } + } + + fn controls(state: &GameState) -> &'static str { + if Self::value(state, "status") == Some("running") { + "[C] Cash out [Q] Forfeit" + } else { + "[SPACE] Play [+/=/-] Bet [Q] Quit" + } + } +} + +impl Game for CrashGame { + fn get_name(&self) -> &'static str { + "Crash" + } + + fn render(&self, state: &GameState) -> String { + let balance = Self::value(state, "balance").unwrap_or("?"); + let multiplier = Self::value(state, "multiplier").unwrap_or("1.00"); + let potential = Self::value(state, "potential") + .map(str::to_string) + .unwrap_or_else(|| state.bet_amount.to_string()); + + if let Some(error) = Self::value(state, "error") { + return format!( + "=== CRASH ===\r\nBalance: {balance} PLN Bet: {} PLN\r\n\r\nERROR: {error}\r\n\r\n[SPACE] Play [+/=/-] Bet [Q] Quit", + state.bet_amount + ); + } + + format!( + "=== CRASH ===\r\nBalance: {} PLN Bet: {} PLN\r\nMultiplier: {}x\r\nPotential: {} PLN\r\n{}\r\n{}", + balance, + state.bet_amount, + multiplier, + potential, + Self::status_text(state), + Self::controls(state) + ) + } + + fn render_lines(&self, _state: &GameState) -> usize { + CRASH_BOARD_LINES + } + + fn get_help(&self) -> String { + "CRASH\n\ + =====\n\ + The multiplier rises from 1.00x and crashes at a hidden point.\n\ + Cash out before it crashes to win bet x multiplier.\n\ + A round can crash at 1.00x, so an instant cashout still risks the bet.\n\ + \n\ + C - cash out at the server's current multiplier\n\ + SPACE - start the next round\n\ + +/- - change the next round's bet\n\ + Q - forfeit an active round (bet is lost), or leave when idle" + .to_string() + } +} diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index 4e0218b..4d9e6a8 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs @@ -1,9 +1,11 @@ use super::blackjack::BlackjackGame; +use super::crash::CrashGame; use super::response::{RouletteResult, SpinResult}; use super::roulette::RouletteGame; use super::slots::SlotsGame; use super::traits::{Game, GameError, GamePhase, GameState}; use crate::blackjack::BlackjackState; +use crate::crash::{payout, CrashState}; use crate::roulette::Bets; use std::collections::HashMap; @@ -18,6 +20,7 @@ impl GameManager { games.insert("slots".to_string(), Box::new(SlotsGame::new())); games.insert("roulette".to_string(), Box::new(RouletteGame::new())); games.insert("blackjack".to_string(), Box::new(BlackjackGame::new())); + games.insert("crash".to_string(), Box::new(CrashGame::new())); Self { current_session: None, @@ -57,6 +60,10 @@ impl GameManager { } else if game_type == "blackjack" { data.insert("status".to_string(), "ready".to_string()); 10 + } else if game_type == "crash" { + data.insert("status".to_string(), "ready".to_string()); + data.insert("multiplier".to_string(), "1.00".to_string()); + 10 } else { 10 }; @@ -173,11 +180,19 @@ impl GameManager { session.data.remove("winnings"); session.data.remove("number"); session.data.remove("color"); + session.data.remove("payout"); + session.data.remove("crash_point"); if session.game_type == "roulette" { session .data .insert("status".to_string(), "place".to_string()); } + if session.game_type == "crash" { + session + .data + .insert("status".to_string(), "ready".to_string()); + session.data.remove("session_id"); + } session.data.insert("error".to_string(), message.into()); } @@ -309,6 +324,81 @@ impl GameManager { } } + pub fn apply_crash_state(&mut self, state: CrashState) { + let Some(session) = self.current_session.as_mut() else { + return; + }; + let running = state.is_running(); + session.phase = if running { + GamePhase::Playing + } else { + GamePhase::Finished + }; + session.bet_amount = state.bet; + session.data.remove("error"); + for key in ["crash_point", "payout"] { + session.data.remove(key); + } + session.data.insert("status".to_string(), state.phase.clone()); + session + .data + .insert("session_id".to_string(), state.session_id); + session + .data + .insert("balance".to_string(), state.balance.to_string()); + session + .data + .insert("multiplier".to_string(), format!("{:.2}", state.multiplier)); + session + .data + .insert("potential".to_string(), state.potential.to_string()); + session + .data + .insert("payout".to_string(), state.payout.to_string()); + if let Some(crash_point) = state.crash_point { + session + .data + .insert("crash_point".to_string(), format!("{crash_point:.2}")); + } + } + + /// Redraw the live multiplier from the client's local clock. + pub fn set_crash_multiplier(&mut self, multiplier: f64) { + let Some(session) = self.current_session.as_mut() else { + return; + }; + let bet = session.bet_amount; + session + .data + .insert("multiplier".to_string(), format!("{multiplier:.2}")); + session + .data + .insert("potential".to_string(), payout(bet, multiplier).to_string()); + } + + pub fn set_crash_status(&mut self, status: &str) { + if let Some(session) = self.current_session.as_mut() { + session + .data + .insert("status".to_string(), status.to_string()); + } + } + + pub fn crash_status(&self) -> String { + self.current_session + .as_ref() + .and_then(|session| session.data.get("status")) + .cloned() + .unwrap_or_else(|| "ready".to_string()) + } + + pub fn crash_session_id(&self) -> Option { + self.current_session + .as_ref() + .and_then(|session| session.data.get("session_id")) + .cloned() + } + pub fn get_current_game(&self) -> Option<&dyn Game> { self.current_session .as_ref() diff --git a/src/client/games/mod.rs b/src/client/games/mod.rs index 40ba32a..e966a25 100644 --- a/src/client/games/mod.rs +++ b/src/client/games/mod.rs @@ -1,4 +1,5 @@ pub mod blackjack; +pub mod crash; pub mod manager; pub mod response; pub mod roulette; diff --git a/src/client/games/response.rs b/src/client/games/response.rs index d9883f7..71e1fed 100644 --- a/src/client/games/response.rs +++ b/src/client/games/response.rs @@ -163,3 +163,24 @@ pub fn parse_blackjack_response( .ok_or_else(|| format!("Unexpected server response: {response}"))?; serde_json::from_str(raw).map_err(|error| format!("Invalid blackjack state: {error}")) } + +pub fn parse_crash_response(response: &str) -> Result { + let response = response.trim(); + if response.is_empty() { + return Err("No response from server".to_string()); + } + if let Some(message) = response.strip_prefix("ERROR") { + let message = message.trim(); + return Err(if message.is_empty() { + "Server error".to_string() + } else { + message.to_string() + }); + } + + let raw = ["CRASH_START ", "CRASH_STATE ", "CRASH_BUSTED "] + .iter() + .find_map(|prefix| response.strip_prefix(prefix)) + .ok_or_else(|| format!("Unexpected server response: {response}"))?; + serde_json::from_str(raw).map_err(|error| format!("Invalid crash state: {error}")) +} -- cgit v1.2.3