diff options
Diffstat (limited to '')
| -rw-r--r-- | src/server/core.rs | 77 | ||||
| -rw-r--r-- | src/server/games/mod.rs | 1 | ||||
| -rw-r--r-- | src/server/games/registry.rs | 2 | ||||
| -rw-r--r-- | src/server/games/roulette.rs | 151 |
4 files changed, 204 insertions, 27 deletions
diff --git a/src/server/core.rs b/src/server/core.rs index 7231577..2ce8b2b 100644 --- a/src/server/core.rs +++ b/src/server/core.rs | |||
| @@ -85,11 +85,11 @@ impl ServerCore { | |||
| 85 | } | 85 | } |
| 86 | } | 86 | } |
| 87 | "PLAY_START" => { | 87 | "PLAY_START" => { |
| 88 | if parts.len() != 3 { | 88 | if parts.len() != 3 && parts.len() != 4 { |
| 89 | "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount>" | 89 | "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount> [payload]" |
| 90 | .to_string() | 90 | .to_string() |
| 91 | } else { | 91 | } else { |
| 92 | self.handle_play_start(state, parts[1], parts[2]) | 92 | self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied()) |
| 93 | } | 93 | } |
| 94 | } | 94 | } |
| 95 | "REGISTER" => { | 95 | "REGISTER" => { |
| @@ -187,6 +187,7 @@ impl ServerCore { | |||
| 187 | state: &ConnectionState, | 187 | state: &ConnectionState, |
| 188 | game_type: &str, | 188 | game_type: &str, |
| 189 | bet_amount_str: &str, | 189 | bet_amount_str: &str, |
| 190 | payload: Option<&str>, | ||
| 190 | ) -> String { | 191 | ) -> String { |
| 191 | let Some(user) = state.user.as_deref() else { | 192 | let Some(user) = state.user.as_deref() else { |
| 192 | return "ERROR Not logged in".to_string(); | 193 | return "ERROR Not logged in".to_string(); |
| @@ -216,7 +217,8 @@ impl ServerCore { | |||
| 216 | } | 217 | } |
| 217 | 218 | ||
| 218 | let game_state = game.prepare_state(user, bet_amount as u64); | 219 | let game_state = game.prepare_state(user, bet_amount as u64); |
| 219 | let new_state = match game.process_action(&game_state, "spin", &[]) { | 220 | let params: Vec<&str> = payload.into_iter().collect(); |
| 221 | let new_state = match game.process_action(&game_state, "spin", ¶ms) { | ||
| 220 | Ok(state) => state, | 222 | Ok(state) => state, |
| 221 | Err(error) => return game_error(error), | 223 | Err(error) => return game_error(error), |
| 222 | }; | 224 | }; |
| @@ -239,26 +241,6 @@ impl ServerCore { | |||
| 239 | } | 241 | } |
| 240 | }; | 242 | }; |
| 241 | 243 | ||
| 242 | let reels = result | ||
| 243 | .result_data | ||
| 244 | .get("reels") | ||
| 245 | .cloned() | ||
| 246 | .unwrap_or_default(); | ||
| 247 | let win_type = result | ||
| 248 | .result_data | ||
| 249 | .get("win_type") | ||
| 250 | .cloned() | ||
| 251 | .unwrap_or_default(); | ||
| 252 | let win_lines = result | ||
| 253 | .result_data | ||
| 254 | .get("win_lines") | ||
| 255 | .cloned() | ||
| 256 | .unwrap_or_else(|| "-".to_string()); | ||
| 257 | let win_cells = result | ||
| 258 | .result_data | ||
| 259 | .get("win_cells") | ||
| 260 | .cloned() | ||
| 261 | .unwrap_or_else(|| "-".to_string()); | ||
| 262 | let details = | 244 | let details = |
| 263 | serde_json::to_string(&result.result_data).unwrap_or_else(|_| "{}".to_string()); | 245 | serde_json::to_string(&result.result_data).unwrap_or_else(|_| "{}".to_string()); |
| 264 | 246 | ||
| @@ -273,9 +255,50 @@ impl ServerCore { | |||
| 273 | ) { | 255 | ) { |
| 274 | Ok(new_balance) => { | 256 | Ok(new_balance) => { |
| 275 | let _ = self.sessions.remove_session(&session_id); | 257 | let _ = self.sessions.remove_session(&session_id); |
| 276 | format!( | 258 | if game_type == "roulette" { |
| 277 | "GAME_RESULT {session_id} {winnings} {bet_amount} {new_balance} {reels} {win_type} {win_lines} {win_cells}" | 259 | let number = result |
| 278 | ) | 260 | .result_data |
| 261 | .get("number") | ||
| 262 | .cloned() | ||
| 263 | .unwrap_or_default(); | ||
| 264 | let color = result | ||
| 265 | .result_data | ||
| 266 | .get("color") | ||
| 267 | .cloned() | ||
| 268 | .unwrap_or_default(); | ||
| 269 | let bets = result | ||
| 270 | .result_data | ||
| 271 | .get("bets") | ||
| 272 | .cloned() | ||
| 273 | .unwrap_or_else(|| "{}".to_string()); | ||
| 274 | format!( | ||
| 275 | "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}" | ||
| 276 | ) | ||
| 277 | } else { | ||
| 278 | let reels = result | ||
| 279 | .result_data | ||
| 280 | .get("reels") | ||
| 281 | .cloned() | ||
| 282 | .unwrap_or_default(); | ||
| 283 | let win_type = result | ||
| 284 | .result_data | ||
| 285 | .get("win_type") | ||
| 286 | .cloned() | ||
| 287 | .unwrap_or_default(); | ||
| 288 | let win_lines = result | ||
| 289 | .result_data | ||
| 290 | .get("win_lines") | ||
| 291 | .cloned() | ||
| 292 | .unwrap_or_else(|| "-".to_string()); | ||
| 293 | let win_cells = result | ||
| 294 | .result_data | ||
| 295 | .get("win_cells") | ||
| 296 | .cloned() | ||
| 297 | .unwrap_or_else(|| "-".to_string()); | ||
| 298 | format!( | ||
| 299 | "GAME_RESULT {session_id} {winnings} {bet_amount} {new_balance} {reels} {win_type} {win_lines} {win_cells}" | ||
| 300 | ) | ||
| 301 | } | ||
| 279 | } | 302 | } |
| 280 | Err(error) => { | 303 | Err(error) => { |
| 281 | let _ = self.sessions.remove_session(&session_id); | 304 | let _ = self.sessions.remove_session(&session_id); |
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 @@ | |||
| 1 | pub mod registry; | 1 | pub mod registry; |
| 2 | pub mod roulette; | ||
| 2 | pub mod session; | 3 | pub mod session; |
| 3 | pub mod slots; | 4 | pub mod slots; |
| 4 | pub mod traits; | 5 | 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 @@ | |||
| 1 | #![allow(dead_code)] | 1 | #![allow(dead_code)] |
| 2 | 2 | ||
| 3 | use super::roulette::RouletteGame; | ||
| 3 | use super::slots::SlotsGame; | 4 | use super::slots::SlotsGame; |
| 4 | use super::traits::Game; | 5 | use super::traits::Game; |
| 5 | use std::collections::HashMap; | 6 | use std::collections::HashMap; |
| @@ -21,6 +22,7 @@ impl GameRegistry { | |||
| 21 | pub fn with_defaults() -> Self { | 22 | pub fn with_defaults() -> Self { |
| 22 | let registry = Self::new(); | 23 | let registry = Self::new(); |
| 23 | registry.register_game(SlotsGame::new()); | 24 | registry.register_game(SlotsGame::new()); |
| 25 | registry.register_game(RouletteGame::new()); | ||
| 24 | registry | 26 | registry |
| 25 | } | 27 | } |
| 26 | 28 | ||
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 | } | ||
