From d9f1fe513e6f7696809d3a9b4d44ca485fe99c97 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 17:18:34 +0100 Subject: v2.1.0: add European roulette with split bets and wheel animation --- src/server/games/mod.rs | 1 + src/server/games/registry.rs | 2 + src/server/games/roulette.rs | 151 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 src/server/games/roulette.rs (limited to 'src/server/games') diff --git a/src/server/games/mod.rs b/src/server/games/mod.rs index 4c62760..35728a1 100644 --- a/src/server/games/mod.rs +++ b/src/server/games/mod.rs @@ -1,4 +1,5 @@ pub mod registry; +pub mod roulette; pub mod session; pub mod slots; pub mod traits; diff --git a/src/server/games/registry.rs b/src/server/games/registry.rs index b7ed9d3..51d446b 100644 --- a/src/server/games/registry.rs +++ b/src/server/games/registry.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] +use super::roulette::RouletteGame; use super::slots::SlotsGame; use super::traits::Game; use std::collections::HashMap; @@ -21,6 +22,7 @@ impl GameRegistry { pub fn with_defaults() -> Self { let registry = Self::new(); registry.register_game(SlotsGame::new()); + registry.register_game(RouletteGame::new()); registry } diff --git a/src/server/games/roulette.rs b/src/server/games/roulette.rs new file mode 100644 index 0000000..7beb8c9 --- /dev/null +++ b/src/server/games/roulette.rs @@ -0,0 +1,151 @@ +use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; +use crate::roulette::{color, Bets}; +use rand::Rng; +use std::collections::HashMap; + +pub struct RouletteGame { + fixed_number: Option, +} + +impl RouletteGame { + pub fn new() -> Self { + Self { fixed_number: None } + } + + pub fn with_result(number: u8) -> Self { + Self { + fixed_number: Some(number), + } + } + + fn spin(&self) -> u8 { + match self.fixed_number { + Some(number) => number, + None => rand::thread_rng().gen_range(0..37), + } + } +} + +impl Game for RouletteGame { + fn game_type(&self) -> &'static str { + "roulette" + } + + fn get_name(&self) -> &'static str { + "Roulette" + } + + fn get_description(&self) -> &'static str { + "European roulette. Bet on red/black, even/odd, or a straight number." + } + + fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState { + let mut data = HashMap::new(); + data.insert("username".to_string(), username.to_string()); + data.insert("status".to_string(), "ready".to_string()); + + GameState { + session_id: String::new(), + game_type: "roulette".to_string(), + bet_amount, + phase: GamePhase::WaitingToStart, + data, + } + } + + fn process_action( + &self, + state: &GameState, + action: &str, + params: &[&str], + ) -> Result { + if state.phase == GamePhase::Finished { + return Err(GameError::GameOver); + } + + match action { + "spin" => { + if state.phase != GamePhase::WaitingToStart + && state.phase != GamePhase::Playing + { + return Err(GameError::InvalidAction( + "Cannot spin in current phase".to_string(), + )); + } + + let payload = params + .first() + .ok_or(GameError::InvalidAction("Missing roulette bets".to_string()))?; + let bets: Bets = serde_json::from_str(payload) + .map_err(|error| GameError::InvalidAction(format!("Invalid bets: {error}")))?; + + if bets.is_empty() { + return Err(GameError::InvalidBet); + } + if let Err(message) = bets.validate(state.bet_amount) { + return Err(GameError::InvalidAction(message)); + } + if bets.total() != state.bet_amount { + return Err(GameError::InvalidBet); + } + + let number = self.spin(); + let bets_json = serde_json::to_string(&bets) + .map_err(|error| GameError::InternalError(error.to_string()))?; + + let mut new_data = state.data.clone(); + new_data.insert("status".to_string(), "spun".to_string()); + new_data.insert("bets".to_string(), bets_json); + new_data.insert("number".to_string(), number.to_string()); + new_data.insert("color".to_string(), color(number).to_string()); + + Ok(GameState { + session_id: state.session_id.clone(), + game_type: state.game_type.clone(), + bet_amount: state.bet_amount, + phase: GamePhase::Finished, + data: new_data, + }) + } + _ => Err(GameError::InvalidAction(format!( + "Unknown action: {action}" + ))), + } + } + + fn calculate_result(&self, state: &GameState) -> Result { + if state.phase != GamePhase::Finished { + return Err(GameError::InternalError("Game not finished".to_string())); + } + + let number: u8 = state + .data + .get("number") + .ok_or(GameError::InternalError("No winning number".to_string()))? + .parse() + .map_err(|_| GameError::InternalError("Invalid winning number".to_string()))?; + let bets_json = state + .data + .get("bets") + .ok_or(GameError::InternalError("No bets".to_string()))? + .clone(); + let bets: Bets = serde_json::from_str(&bets_json) + .map_err(|error| GameError::InternalError(format!("Invalid bets: {error}")))?; + + let winnings = bets.winnings(number); + + let mut result_data = HashMap::new(); + result_data.insert("number".to_string(), number.to_string()); + result_data.insert("color".to_string(), color(number).to_string()); + result_data.insert("bets".to_string(), bets_json); + + Ok(GameResult { + session_id: state.session_id.clone(), + game_type: state.game_type.clone(), + bet_amount: state.bet_amount, + winnings, + final_balance: 0, + result_data, + }) + } +} -- cgit v1.2.3