diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-29 17:18:34 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-29 17:18:34 +0100 |
| commit | d9f1fe513e6f7696809d3a9b4d44ca485fe99c97 (patch) | |
| tree | 9da99200fdfe004a47056d10340b3f59c7ccc52e /src/server/games/roulette.rs | |
| parent | v2.0.0: reset plays audit to a generic per-game schema (diff) | |
| download | zagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.tar.gz zagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.zip | |
v2.1.0: add European roulette with split bets and wheel animation
Diffstat (limited to 'src/server/games/roulette.rs')
| -rw-r--r-- | src/server/games/roulette.rs | 151 |
1 files changed, 151 insertions, 0 deletions
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 @@ | |||
| 1 | use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; | ||
| 2 | use crate::roulette::{color, Bets}; | ||
| 3 | use rand::Rng; | ||
| 4 | use std::collections::HashMap; | ||
| 5 | |||
| 6 | pub struct RouletteGame { | ||
| 7 | fixed_number: Option<u8>, | ||
| 8 | } | ||
| 9 | |||
| 10 | impl RouletteGame { | ||
| 11 | pub fn new() -> Self { | ||
| 12 | Self { fixed_number: None } | ||
| 13 | } | ||
| 14 | |||
| 15 | pub fn with_result(number: u8) -> Self { | ||
| 16 | Self { | ||
| 17 | fixed_number: Some(number), | ||
| 18 | } | ||
| 19 | } | ||
| 20 | |||
| 21 | fn spin(&self) -> u8 { | ||
| 22 | match self.fixed_number { | ||
| 23 | Some(number) => number, | ||
| 24 | None => rand::thread_rng().gen_range(0..37), | ||
| 25 | } | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | impl Game for RouletteGame { | ||
| 30 | fn game_type(&self) -> &'static str { | ||
| 31 | "roulette" | ||
| 32 | } | ||
| 33 | |||
| 34 | fn get_name(&self) -> &'static str { | ||
| 35 | "Roulette" | ||
| 36 | } | ||
| 37 | |||
| 38 | fn get_description(&self) -> &'static str { | ||
| 39 | "European roulette. Bet on red/black, even/odd, or a straight number." | ||
| 40 | } | ||
| 41 | |||
| 42 | fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState { | ||
| 43 | let mut data = HashMap::new(); | ||
| 44 | data.insert("username".to_string(), username.to_string()); | ||
| 45 | data.insert("status".to_string(), "ready".to_string()); | ||
| 46 | |||
| 47 | GameState { | ||
| 48 | session_id: String::new(), | ||
| 49 | game_type: "roulette".to_string(), | ||
| 50 | bet_amount, | ||
| 51 | phase: GamePhase::WaitingToStart, | ||
| 52 | data, | ||
| 53 | } | ||
| 54 | } | ||
| 55 | |||
| 56 | fn process_action( | ||
| 57 | &self, | ||
| 58 | state: &GameState, | ||
| 59 | action: &str, | ||
| 60 | params: &[&str], | ||
| 61 | ) -> Result<GameState, GameError> { | ||
| 62 | if state.phase == GamePhase::Finished { | ||
| 63 | return Err(GameError::GameOver); | ||
| 64 | } | ||
| 65 | |||
| 66 | match action { | ||
| 67 | "spin" => { | ||
| 68 | if state.phase != GamePhase::WaitingToStart | ||
| 69 | && state.phase != GamePhase::Playing | ||
| 70 | { | ||
| 71 | return Err(GameError::InvalidAction( | ||
| 72 | "Cannot spin in current phase".to_string(), | ||
| 73 | )); | ||
| 74 | } | ||
| 75 | |||
| 76 | let payload = params | ||
| 77 | .first() | ||
| 78 | .ok_or(GameError::InvalidAction("Missing roulette bets".to_string()))?; | ||
| 79 | let bets: Bets = serde_json::from_str(payload) | ||
| 80 | .map_err(|error| GameError::InvalidAction(format!("Invalid bets: {error}")))?; | ||
| 81 | |||
| 82 | if bets.is_empty() { | ||
| 83 | return Err(GameError::InvalidBet); | ||
| 84 | } | ||
| 85 | if let Err(message) = bets.validate(state.bet_amount) { | ||
| 86 | return Err(GameError::InvalidAction(message)); | ||
| 87 | } | ||
| 88 | if bets.total() != state.bet_amount { | ||
| 89 | return Err(GameError::InvalidBet); | ||
| 90 | } | ||
| 91 | |||
| 92 | let number = self.spin(); | ||
| 93 | let bets_json = serde_json::to_string(&bets) | ||
| 94 | .map_err(|error| GameError::InternalError(error.to_string()))?; | ||
| 95 | |||
| 96 | let mut new_data = state.data.clone(); | ||
| 97 | new_data.insert("status".to_string(), "spun".to_string()); | ||
| 98 | new_data.insert("bets".to_string(), bets_json); | ||
| 99 | new_data.insert("number".to_string(), number.to_string()); | ||
| 100 | new_data.insert("color".to_string(), color(number).to_string()); | ||
| 101 | |||
| 102 | Ok(GameState { | ||
| 103 | session_id: state.session_id.clone(), | ||
| 104 | game_type: state.game_type.clone(), | ||
| 105 | bet_amount: state.bet_amount, | ||
| 106 | phase: GamePhase::Finished, | ||
| 107 | data: new_data, | ||
| 108 | }) | ||
| 109 | } | ||
| 110 | _ => Err(GameError::InvalidAction(format!( | ||
| 111 | "Unknown action: {action}" | ||
| 112 | ))), | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError> { | ||
| 117 | if state.phase != GamePhase::Finished { | ||
| 118 | return Err(GameError::InternalError("Game not finished".to_string())); | ||
| 119 | } | ||
| 120 | |||
| 121 | let number: u8 = state | ||
| 122 | .data | ||
| 123 | .get("number") | ||
| 124 | .ok_or(GameError::InternalError("No winning number".to_string()))? | ||
| 125 | .parse() | ||
| 126 | .map_err(|_| GameError::InternalError("Invalid winning number".to_string()))?; | ||
| 127 | let bets_json = state | ||
| 128 | .data | ||
| 129 | .get("bets") | ||
| 130 | .ok_or(GameError::InternalError("No bets".to_string()))? | ||
| 131 | .clone(); | ||
| 132 | let bets: Bets = serde_json::from_str(&bets_json) | ||
| 133 | .map_err(|error| GameError::InternalError(format!("Invalid bets: {error}")))?; | ||
| 134 | |||
| 135 | let winnings = bets.winnings(number); | ||
| 136 | |||
| 137 | let mut result_data = HashMap::new(); | ||
| 138 | result_data.insert("number".to_string(), number.to_string()); | ||
| 139 | result_data.insert("color".to_string(), color(number).to_string()); | ||
| 140 | result_data.insert("bets".to_string(), bets_json); | ||
| 141 | |||
| 142 | Ok(GameResult { | ||
| 143 | session_id: state.session_id.clone(), | ||
| 144 | game_type: state.game_type.clone(), | ||
| 145 | bet_amount: state.bet_amount, | ||
| 146 | winnings, | ||
| 147 | final_balance: 0, | ||
| 148 | result_data, | ||
| 149 | }) | ||
| 150 | } | ||
| 151 | } | ||
