diff options
Diffstat (limited to '')
| -rw-r--r-- | src/client/games/manager.rs | 131 | ||||
| -rw-r--r-- | src/client/games/mod.rs | 1 | ||||
| -rw-r--r-- | src/client/games/response.rs | 64 | ||||
| -rw-r--r-- | src/client/games/roulette.rs | 157 |
4 files changed, 350 insertions, 3 deletions
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index 168c49d..02e9451 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs | |||
| @@ -1,6 +1,8 @@ | |||
| 1 | use super::response::SpinResult; | 1 | use super::response::{RouletteResult, SpinResult}; |
| 2 | use super::roulette::RouletteGame; | ||
| 2 | use super::slots::SlotsGame; | 3 | use super::slots::SlotsGame; |
| 3 | use super::traits::{Game, GameError, GamePhase, GameState}; | 4 | use super::traits::{Game, GameError, GamePhase, GameState}; |
| 5 | use crate::roulette::Bets; | ||
| 4 | use std::collections::HashMap; | 6 | use std::collections::HashMap; |
| 5 | 7 | ||
| 6 | pub struct GameManager { | 8 | pub struct GameManager { |
| @@ -12,6 +14,7 @@ impl GameManager { | |||
| 12 | pub fn new() -> Self { | 14 | pub fn new() -> Self { |
| 13 | let mut games: HashMap<String, Box<dyn Game>> = HashMap::new(); | 15 | let mut games: HashMap<String, Box<dyn Game>> = HashMap::new(); |
| 14 | games.insert("slots".to_string(), Box::new(SlotsGame::new())); | 16 | games.insert("slots".to_string(), Box::new(SlotsGame::new())); |
| 17 | games.insert("roulette".to_string(), Box::new(RouletteGame::new())); | ||
| 15 | 18 | ||
| 16 | Self { | 19 | Self { |
| 17 | current_session: None, | 20 | current_session: None, |
| @@ -38,11 +41,25 @@ impl GameManager { | |||
| 38 | return Err(GameError::InvalidAction); | 41 | return Err(GameError::InvalidAction); |
| 39 | } | 42 | } |
| 40 | 43 | ||
| 44 | let mut data = HashMap::new(); | ||
| 45 | let bet_amount = if game_type == "roulette" { | ||
| 46 | data.insert( | ||
| 47 | "bets".to_string(), | ||
| 48 | serde_json::to_string(&Bets::default()).unwrap_or_else(|_| "{}".to_string()), | ||
| 49 | ); | ||
| 50 | data.insert("chip".to_string(), "10".to_string()); | ||
| 51 | data.insert("status".to_string(), "place".to_string()); | ||
| 52 | data.insert("number_input".to_string(), String::new()); | ||
| 53 | 0 | ||
| 54 | } else { | ||
| 55 | 10 | ||
| 56 | }; | ||
| 57 | |||
| 41 | self.current_session = Some(GameState { | 58 | self.current_session = Some(GameState { |
| 42 | game_type: game_type.to_string(), | 59 | game_type: game_type.to_string(), |
| 43 | bet_amount: 10, | 60 | bet_amount, |
| 44 | phase: GamePhase::WaitingToStart, | 61 | phase: GamePhase::WaitingToStart, |
| 45 | data: HashMap::new(), | 62 | data, |
| 46 | }); | 63 | }); |
| 47 | 64 | ||
| 48 | Ok(()) | 65 | Ok(()) |
| @@ -54,6 +71,17 @@ impl GameManager { | |||
| 54 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { | 71 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { |
| 55 | return None; | 72 | return None; |
| 56 | } | 73 | } |
| 74 | if session.game_type == "roulette" { | ||
| 75 | let bets = session | ||
| 76 | .data | ||
| 77 | .get("bets") | ||
| 78 | .cloned() | ||
| 79 | .unwrap_or_else(|| "{}".to_string()); | ||
| 80 | return Some(format!( | ||
| 81 | "PLAY_START roulette {} {}", | ||
| 82 | session.bet_amount, bets | ||
| 83 | )); | ||
| 84 | } | ||
| 57 | Some(format!( | 85 | Some(format!( |
| 58 | "PLAY_START {} {}", | 86 | "PLAY_START {} {}", |
| 59 | session.game_type, session.bet_amount | 87 | session.game_type, session.bet_amount |
| @@ -78,6 +106,13 @@ impl GameManager { | |||
| 78 | session.data.remove("win_lines"); | 106 | session.data.remove("win_lines"); |
| 79 | session.data.remove("win_cells"); | 107 | session.data.remove("win_cells"); |
| 80 | session.data.remove("winnings"); | 108 | session.data.remove("winnings"); |
| 109 | session.data.remove("number"); | ||
| 110 | session.data.remove("color"); | ||
| 111 | if session.game_type == "roulette" { | ||
| 112 | session | ||
| 113 | .data | ||
| 114 | .insert("status".to_string(), "spinning".to_string()); | ||
| 115 | } | ||
| 81 | Ok(()) | 116 | Ok(()) |
| 82 | } | 117 | } |
| 83 | 118 | ||
| @@ -130,6 +165,13 @@ impl GameManager { | |||
| 130 | session.data.remove("win_lines"); | 165 | session.data.remove("win_lines"); |
| 131 | session.data.remove("win_cells"); | 166 | session.data.remove("win_cells"); |
| 132 | session.data.remove("winnings"); | 167 | session.data.remove("winnings"); |
| 168 | session.data.remove("number"); | ||
| 169 | session.data.remove("color"); | ||
| 170 | if session.game_type == "roulette" { | ||
| 171 | session | ||
| 172 | .data | ||
| 173 | .insert("status".to_string(), "place".to_string()); | ||
| 174 | } | ||
| 133 | session.data.insert("error".to_string(), message.into()); | 175 | session.data.insert("error".to_string(), message.into()); |
| 134 | } | 176 | } |
| 135 | 177 | ||
| @@ -141,6 +183,83 @@ impl GameManager { | |||
| 141 | } | 183 | } |
| 142 | } | 184 | } |
| 143 | 185 | ||
| 186 | pub fn roulette_bets(&self) -> Bets { | ||
| 187 | self.current_session | ||
| 188 | .as_ref() | ||
| 189 | .and_then(|session| session.data.get("bets")) | ||
| 190 | .and_then(|raw| serde_json::from_str(raw).ok()) | ||
| 191 | .unwrap_or_default() | ||
| 192 | } | ||
| 193 | |||
| 194 | pub fn set_roulette_bets(&mut self, bets: &Bets) { | ||
| 195 | if let Some(session) = self.current_session.as_mut() { | ||
| 196 | session.data.insert( | ||
| 197 | "bets".to_string(), | ||
| 198 | serde_json::to_string(bets).unwrap_or_else(|_| "{}".to_string()), | ||
| 199 | ); | ||
| 200 | session.bet_amount = bets.total(); | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | pub fn roulette_chip(&self) -> u64 { | ||
| 205 | self.current_session | ||
| 206 | .as_ref() | ||
| 207 | .and_then(|session| session.data.get("chip")) | ||
| 208 | .and_then(|value| value.parse().ok()) | ||
| 209 | .unwrap_or(10) | ||
| 210 | } | ||
| 211 | |||
| 212 | pub fn set_roulette_chip(&mut self, chip: u64) { | ||
| 213 | if let Some(session) = self.current_session.as_mut() { | ||
| 214 | session.data.insert("chip".to_string(), chip.to_string()); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | |||
| 218 | pub fn set_roulette_number_input(&mut self, value: &str) { | ||
| 219 | if let Some(session) = self.current_session.as_mut() { | ||
| 220 | session | ||
| 221 | .data | ||
| 222 | .insert("number_input".to_string(), value.to_string()); | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | pub fn set_roulette_status(&mut self, status: &str) { | ||
| 227 | if let Some(session) = self.current_session.as_mut() { | ||
| 228 | session | ||
| 229 | .data | ||
| 230 | .insert("status".to_string(), status.to_string()); | ||
| 231 | if status != "error" { | ||
| 232 | session.data.remove("error"); | ||
| 233 | } | ||
| 234 | } | ||
| 235 | } | ||
| 236 | |||
| 237 | pub fn apply_roulette_result(&mut self, result: RouletteResult) { | ||
| 238 | let Some(session) = self.current_session.as_mut() else { | ||
| 239 | return; | ||
| 240 | }; | ||
| 241 | session.phase = GamePhase::Finished; | ||
| 242 | session.bet_amount = result.bet_amount; | ||
| 243 | session.data.remove("error"); | ||
| 244 | session.data.insert("bets".to_string(), result.bets); | ||
| 245 | session | ||
| 246 | .data | ||
| 247 | .insert("number".to_string(), result.number.to_string()); | ||
| 248 | session.data.insert("color".to_string(), result.color); | ||
| 249 | session | ||
| 250 | .data | ||
| 251 | .insert("winnings".to_string(), result.winnings.to_string()); | ||
| 252 | session | ||
| 253 | .data | ||
| 254 | .insert("balance".to_string(), result.balance.to_string()); | ||
| 255 | session | ||
| 256 | .data | ||
| 257 | .insert("status".to_string(), "result".to_string()); | ||
| 258 | session | ||
| 259 | .data | ||
| 260 | .insert("number_input".to_string(), String::new()); | ||
| 261 | } | ||
| 262 | |||
| 144 | pub fn get_current_game(&self) -> Option<&dyn Game> { | 263 | pub fn get_current_game(&self) -> Option<&dyn Game> { |
| 145 | self.current_session | 264 | self.current_session |
| 146 | .as_ref() | 265 | .as_ref() |
| @@ -167,6 +286,12 @@ impl GameManager { | |||
| 167 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { | 286 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { |
| 168 | return Err(GameError::InvalidAction); | 287 | return Err(GameError::InvalidAction); |
| 169 | } | 288 | } |
| 289 | if session.game_type == "roulette" { | ||
| 290 | session | ||
| 291 | .data | ||
| 292 | .insert("chip".to_string(), bet.max(10).to_string()); | ||
| 293 | return Ok(bet.max(10)); | ||
| 294 | } | ||
| 170 | session.bet_amount = bet.max(10); | 295 | session.bet_amount = bet.max(10); |
| 171 | Ok(session.bet_amount) | 296 | Ok(session.bet_amount) |
| 172 | } | 297 | } |
diff --git a/src/client/games/mod.rs b/src/client/games/mod.rs index 63cc579..131ffbb 100644 --- a/src/client/games/mod.rs +++ b/src/client/games/mod.rs | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | pub mod manager; | 1 | pub mod manager; |
| 2 | pub mod response; | 2 | pub mod response; |
| 3 | pub mod roulette; | ||
| 3 | pub mod slots; | 4 | pub mod slots; |
| 4 | pub mod traits; | 5 | pub mod traits; |
diff --git a/src/client/games/response.rs b/src/client/games/response.rs index d7f9d45..73080dd 100644 --- a/src/client/games/response.rs +++ b/src/client/games/response.rs | |||
| @@ -77,3 +77,67 @@ fn parse_indices(raw: &str) -> Vec<usize> { | |||
| 77 | .filter_map(|value| value.trim().parse::<usize>().ok()) | 77 | .filter_map(|value| value.trim().parse::<usize>().ok()) |
| 78 | .collect() | 78 | .collect() |
| 79 | } | 79 | } |
| 80 | |||
| 81 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 82 | pub struct RouletteResult { | ||
| 83 | pub session_id: String, | ||
| 84 | pub winnings: u64, | ||
| 85 | pub bet_amount: u64, | ||
| 86 | pub balance: i64, | ||
| 87 | pub number: u8, | ||
| 88 | pub color: String, | ||
| 89 | pub bets: String, | ||
| 90 | } | ||
| 91 | |||
| 92 | pub fn parse_roulette_response(response: &str) -> Result<RouletteResult, String> { | ||
| 93 | let response = response.trim(); | ||
| 94 | if response.is_empty() { | ||
| 95 | return Err("No response from server".to_string()); | ||
| 96 | } | ||
| 97 | if let Some(message) = response.strip_prefix("ERROR") { | ||
| 98 | let message = message.trim(); | ||
| 99 | return Err(if message.is_empty() { | ||
| 100 | "Server error".to_string() | ||
| 101 | } else { | ||
| 102 | message.to_string() | ||
| 103 | }); | ||
| 104 | } | ||
| 105 | |||
| 106 | let parts: Vec<&str> = response.split_whitespace().collect(); | ||
| 107 | if parts.first() != Some(&"ROULETTE_RESULT") || parts.len() < 8 { | ||
| 108 | return Err(format!("Unexpected server response: {response}")); | ||
| 109 | } | ||
| 110 | |||
| 111 | let winnings = parts[2] | ||
| 112 | .parse::<u64>() | ||
| 113 | .map_err(|_| format!("Invalid winnings from server: {}", parts[2]))?; | ||
| 114 | let bet_amount = parts[3] | ||
| 115 | .parse::<u64>() | ||
| 116 | .map_err(|_| format!("Invalid bet from server: {}", parts[3]))?; | ||
| 117 | let balance = parts[4] | ||
| 118 | .parse::<i64>() | ||
| 119 | .map_err(|_| format!("Invalid balance from server: {}", parts[4]))?; | ||
| 120 | let number = parts[5] | ||
| 121 | .parse::<u8>() | ||
| 122 | .map_err(|_| format!("Invalid roulette number: {}", parts[5]))?; | ||
| 123 | if number > 36 { | ||
| 124 | return Err(format!("Invalid roulette number: {number}")); | ||
| 125 | } | ||
| 126 | let color = parts[6].to_string(); | ||
| 127 | if !matches!(color.as_str(), "green" | "red" | "black") { | ||
| 128 | return Err(format!("Invalid roulette color: {color}")); | ||
| 129 | } | ||
| 130 | let bets = parts[7].to_string(); | ||
| 131 | let _: crate::roulette::Bets = serde_json::from_str(&bets) | ||
| 132 | .map_err(|error| format!("Invalid roulette bets: {error}"))?; | ||
| 133 | |||
| 134 | Ok(RouletteResult { | ||
| 135 | session_id: parts[1].to_string(), | ||
| 136 | winnings, | ||
| 137 | bet_amount, | ||
| 138 | balance, | ||
| 139 | number, | ||
| 140 | color, | ||
| 141 | bets, | ||
| 142 | }) | ||
| 143 | } | ||
diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs new file mode 100644 index 0000000..3379317 --- /dev/null +++ b/src/client/games/roulette.rs | |||
| @@ -0,0 +1,157 @@ | |||
| 1 | use super::traits::{Game, GameState}; | ||
| 2 | use crate::roulette::{is_even, is_odd, Bets}; | ||
| 3 | |||
| 4 | pub const ROULETTE_BOARD_LINES: usize = 8; | ||
| 5 | |||
| 6 | pub struct RouletteGame; | ||
| 7 | |||
| 8 | impl RouletteGame { | ||
| 9 | pub fn new() -> Self { | ||
| 10 | Self | ||
| 11 | } | ||
| 12 | |||
| 13 | fn bets(state: &GameState) -> Bets { | ||
| 14 | state | ||
| 15 | .data | ||
| 16 | .get("bets") | ||
| 17 | .and_then(|raw| serde_json::from_str(raw).ok()) | ||
| 18 | .unwrap_or_default() | ||
| 19 | } | ||
| 20 | |||
| 21 | fn bets_line(bets: &Bets) -> String { | ||
| 22 | let mut parts = Vec::new(); | ||
| 23 | if bets.red > 0 { | ||
| 24 | parts.push(format!("RED {}", bets.red)); | ||
| 25 | } | ||
| 26 | if bets.black > 0 { | ||
| 27 | parts.push(format!("BLACK {}", bets.black)); | ||
| 28 | } | ||
| 29 | if bets.even > 0 { | ||
| 30 | parts.push(format!("EVEN {}", bets.even)); | ||
| 31 | } | ||
| 32 | if bets.odd > 0 { | ||
| 33 | parts.push(format!("ODD {}", bets.odd)); | ||
| 34 | } | ||
| 35 | for bet in &bets.numbers { | ||
| 36 | parts.push(format!("#{} {}", bet.number, bet.amount)); | ||
| 37 | } | ||
| 38 | |||
| 39 | let list = if parts.is_empty() { | ||
| 40 | "(none)".to_string() | ||
| 41 | } else { | ||
| 42 | parts.join(" ") | ||
| 43 | }; | ||
| 44 | format!("Bets: {} Total: {} PLN", list, bets.total()) | ||
| 45 | } | ||
| 46 | |||
| 47 | fn result_lines(state: &GameState) -> (String, String) { | ||
| 48 | if let Some(error) = state.data.get("error") { | ||
| 49 | if !error.is_empty() { | ||
| 50 | return (format!("ERROR: {error}"), String::new()); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | match state.data.get("status").map(String::as_str) { | ||
| 55 | Some("result") => { | ||
| 56 | let number = state | ||
| 57 | .data | ||
| 58 | .get("number") | ||
| 59 | .map(String::as_str) | ||
| 60 | .unwrap_or("?"); | ||
| 61 | let color = state | ||
| 62 | .data | ||
| 63 | .get("color") | ||
| 64 | .map(|value| value.to_uppercase()) | ||
| 65 | .unwrap_or_default(); | ||
| 66 | let parity = number | ||
| 67 | .parse::<u8>() | ||
| 68 | .ok() | ||
| 69 | .map(|number| { | ||
| 70 | if is_even(number) { | ||
| 71 | " EVEN" | ||
| 72 | } else if is_odd(number) { | ||
| 73 | " ODD" | ||
| 74 | } else { | ||
| 75 | "" | ||
| 76 | } | ||
| 77 | }) | ||
| 78 | .unwrap_or(""); | ||
| 79 | let winnings = state | ||
| 80 | .data | ||
| 81 | .get("winnings") | ||
| 82 | .map(String::as_str) | ||
| 83 | .unwrap_or("0"); | ||
| 84 | let result = if winnings == "0" { | ||
| 85 | "No win.".to_string() | ||
| 86 | } else { | ||
| 87 | format!("Won {winnings} PLN") | ||
| 88 | }; | ||
| 89 | (format!("Winning: {number} {color}{parity}"), result) | ||
| 90 | } | ||
| 91 | Some("spinning") => ("Spinning...".to_string(), String::new()), | ||
| 92 | _ => ("Place bets, then SPACE to spin.".to_string(), String::new()), | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | impl Game for RouletteGame { | ||
| 98 | fn get_name(&self) -> &'static str { | ||
| 99 | "Roulette" | ||
| 100 | } | ||
| 101 | |||
| 102 | fn render(&self, state: &GameState) -> String { | ||
| 103 | let bets = Self::bets(state); | ||
| 104 | let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); | ||
| 105 | let balance = state | ||
| 106 | .data | ||
| 107 | .get("balance") | ||
| 108 | .map(String::as_str) | ||
| 109 | .unwrap_or("?"); | ||
| 110 | let input = state | ||
| 111 | .data | ||
| 112 | .get("number_input") | ||
| 113 | .map(String::as_str) | ||
| 114 | .unwrap_or(""); | ||
| 115 | let input_line = if input.is_empty() { | ||
| 116 | String::new() | ||
| 117 | } else { | ||
| 118 | format!("Number: {input}_") | ||
| 119 | }; | ||
| 120 | let (status, result) = Self::result_lines(state); | ||
| 121 | |||
| 122 | format!( | ||
| 123 | "=== ROULETTE ===\r\nBalance: {} PLN Chip: {} PLN\r\n{}\r\n{}\r\n{}\r\n{}\r\n[R]ed [B]lack [E]ven [O]dd [#]Number [C]lear\r\n[+/-] Chip [SPACE] Spin [Q] Quit", | ||
| 124 | balance, | ||
| 125 | chip, | ||
| 126 | Self::bets_line(&bets), | ||
| 127 | input_line, | ||
| 128 | status, | ||
| 129 | result | ||
| 130 | ) | ||
| 131 | } | ||
| 132 | |||
| 133 | fn render_lines(&self, _state: &GameState) -> usize { | ||
| 134 | ROULETTE_BOARD_LINES | ||
| 135 | } | ||
| 136 | |||
| 137 | fn get_help(&self) -> String { | ||
| 138 | "ROULETTE (European)\n\ | ||
| 139 | ==================\n\ | ||
| 140 | 37 pockets: 0 plus 1-36. 0 is green and loses all outside bets.\n\ | ||
| 141 | \n\ | ||
| 142 | Betting:\n\ | ||
| 143 | R / B / E / O - add the current chip to red/black/even/odd\n\ | ||
| 144 | digits + Enter - add the current chip to that number\n\ | ||
| 145 | C - clear all bets\n\ | ||
| 146 | + or = / - - change the chip size\n\ | ||
| 147 | SPACE - spin when at least one bet is placed\n\ | ||
| 148 | Q - exit\n\ | ||
| 149 | \n\ | ||
| 150 | Payouts:\n\ | ||
| 151 | straight number: 35:1 (36x returned)\n\ | ||
| 152 | red/black: 1:1 (2x returned)\n\ | ||
| 153 | even/odd: 1:1 (2x returned)\n\ | ||
| 154 | Multiple bets are settled independently and added together." | ||
| 155 | .to_string() | ||
| 156 | } | ||
| 157 | } | ||
