diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 22:28:20 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 22:28:20 +0100 |
| commit | 86d5f832c8452aad2313489b82eb8fd514215b95 (patch) | |
| tree | 596a9be39c3d91ebc20cc8b877549e3d10895901 /src/client/games/manager.rs | |
| parent | v0.2.2: split client/server logic into the library (diff) | |
| download | zagan-bet-86d5f832c8452aad2313489b82eb8fd514215b95.tar.gz zagan-bet-86d5f832c8452aad2313489b82eb8fd514215b95.zip | |
v0.2.3: add ClientCore and fix the game board rendering
Diffstat (limited to 'src/client/games/manager.rs')
| -rw-r--r-- | src/client/games/manager.rs | 140 |
1 files changed, 87 insertions, 53 deletions
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index be96b1a..71cd28c 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs | |||
| @@ -1,11 +1,10 @@ | |||
| 1 | use super::response::SpinResult; | ||
| 1 | use super::slots::SlotsGame; | 2 | use super::slots::SlotsGame; |
| 2 | use super::traits::{Game, GameError, GamePhase, GameState}; | 3 | use super::traits::{Game, GameError, GamePhase, GameState}; |
| 3 | use std::collections::HashMap; | 4 | use std::collections::HashMap; |
| 4 | use std::io::Write; | ||
| 5 | 5 | ||
| 6 | pub struct GameManager { | 6 | pub struct GameManager { |
| 7 | current_session: Option<GameState>, | 7 | current_session: Option<GameState>, |
| 8 | pending_game_type: Option<String>, | ||
| 9 | games: HashMap<String, Box<dyn Game>>, | 8 | games: HashMap<String, Box<dyn Game>>, |
| 10 | } | 9 | } |
| 11 | 10 | ||
| @@ -16,16 +15,18 @@ impl GameManager { | |||
| 16 | 15 | ||
| 17 | Self { | 16 | Self { |
| 18 | current_session: None, | 17 | current_session: None, |
| 19 | pending_game_type: None, | ||
| 20 | games, | 18 | games, |
| 21 | } | 19 | } |
| 22 | } | 20 | } |
| 23 | 21 | ||
| 24 | pub fn get_available_games(&self) -> Vec<(&str, &str)> { | 22 | pub fn get_available_games(&self) -> Vec<(&str, &str)> { |
| 25 | self.games | 23 | let mut games: Vec<(&str, &str)> = self |
| 24 | .games | ||
| 26 | .iter() | 25 | .iter() |
| 27 | .map(|(k, v)| (k.as_str(), v.get_name())) | 26 | .map(|(key, game)| (key.as_str(), game.get_name())) |
| 28 | .collect() | 27 | .collect(); |
| 28 | games.sort_unstable_by_key(|(key, _)| *key); | ||
| 29 | games | ||
| 29 | } | 30 | } |
| 30 | 31 | ||
| 31 | pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> { | 32 | pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> { |
| @@ -37,8 +38,6 @@ impl GameManager { | |||
| 37 | return Err(GameError::InvalidAction); | 38 | return Err(GameError::InvalidAction); |
| 38 | } | 39 | } |
| 39 | 40 | ||
| 40 | self.pending_game_type = Some(game_type.to_string()); | ||
| 41 | |||
| 42 | self.current_session = Some(GameState { | 41 | self.current_session = Some(GameState { |
| 43 | game_type: game_type.to_string(), | 42 | game_type: game_type.to_string(), |
| 44 | bet_amount: 10, | 43 | bet_amount: 10, |
| @@ -49,28 +48,75 @@ impl GameManager { | |||
| 49 | Ok(()) | 48 | Ok(()) |
| 50 | } | 49 | } |
| 51 | 50 | ||
| 52 | pub fn start_game<W: Write>(&mut self, stream: &mut W) -> Result<(), GameError> { | 51 | /// The wire request for a spin, if the current phase allows one. |
| 53 | let game_type = self | 52 | pub fn spin_request(&self) -> Option<String> { |
| 54 | .pending_game_type | 53 | let session = self.current_session.as_ref()?; |
| 55 | .as_ref() | 54 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { |
| 56 | .ok_or(GameError::InvalidAction)?; | 55 | return None; |
| 56 | } | ||
| 57 | Some(format!( | ||
| 58 | "PLAY_START {} {}", | ||
| 59 | session.game_type, session.bet_amount | ||
| 60 | )) | ||
| 61 | } | ||
| 57 | 62 | ||
| 58 | let state = self | 63 | /// Move the board into its spinning state and drop the previous result |
| 64 | /// so stale reels/winnings cannot leak into the new spin. | ||
| 65 | pub fn begin_spin(&mut self) -> Result<(), GameError> { | ||
| 66 | let session = self | ||
| 59 | .current_session | 67 | .current_session |
| 60 | .as_mut() | 68 | .as_mut() |
| 61 | .ok_or(GameError::InvalidAction)?; | 69 | .ok_or(GameError::InvalidAction)?; |
| 62 | 70 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { | |
| 63 | if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Finished { | ||
| 64 | return Err(GameError::InvalidAction); | 71 | return Err(GameError::InvalidAction); |
| 65 | } | 72 | } |
| 66 | 73 | ||
| 67 | let bet_amount = state.bet_amount; | 74 | session.phase = GamePhase::Playing; |
| 75 | session.data.remove("error"); | ||
| 76 | session.data.remove("reels"); | ||
| 77 | session.data.remove("win_type"); | ||
| 78 | session.data.remove("winnings"); | ||
| 79 | Ok(()) | ||
| 80 | } | ||
| 68 | 81 | ||
| 69 | let writer = stream; | 82 | /// Store the settled spin so `render()` can draw it in place. |
| 70 | writeln!(writer, "PLAY_START {} {}", game_type, bet_amount) | 83 | pub fn apply_result(&mut self, result: SpinResult) { |
| 71 | .map_err(|_| GameError::NetworkError)?; | 84 | let Some(session) = self.current_session.as_mut() else { |
| 85 | return; | ||
| 86 | }; | ||
| 87 | session.phase = GamePhase::Finished; | ||
| 88 | session.bet_amount = result.bet_amount; | ||
| 89 | session.data.remove("error"); | ||
| 90 | session.data.insert("reels".to_string(), result.reels); | ||
| 91 | session | ||
| 92 | .data | ||
| 93 | .insert("win_type".to_string(), result.win_type); | ||
| 94 | session | ||
| 95 | .data | ||
| 96 | .insert("winnings".to_string(), result.winnings.to_string()); | ||
| 97 | session | ||
| 98 | .data | ||
| 99 | .insert("balance".to_string(), result.balance.to_string()); | ||
| 100 | } | ||
| 72 | 101 | ||
| 73 | Ok(()) | 102 | /// Show a failed spin without leaving the player stuck in `Playing`. |
| 103 | pub fn set_error(&mut self, message: impl Into<String>) { | ||
| 104 | let Some(session) = self.current_session.as_mut() else { | ||
| 105 | return; | ||
| 106 | }; | ||
| 107 | session.phase = GamePhase::WaitingToStart; | ||
| 108 | session.data.remove("reels"); | ||
| 109 | session.data.remove("win_type"); | ||
| 110 | session.data.remove("winnings"); | ||
| 111 | session.data.insert("error".to_string(), message.into()); | ||
| 112 | } | ||
| 113 | |||
| 114 | pub fn set_balance(&mut self, balance: i64) { | ||
| 115 | if let Some(session) = self.current_session.as_mut() { | ||
| 116 | session | ||
| 117 | .data | ||
| 118 | .insert("balance".to_string(), balance.to_string()); | ||
| 119 | } | ||
| 74 | } | 120 | } |
| 75 | 121 | ||
| 76 | pub fn get_current_game(&self) -> Option<&dyn Game> { | 122 | pub fn get_current_game(&self) -> Option<&dyn Game> { |
| @@ -83,49 +129,37 @@ impl GameManager { | |||
| 83 | self.current_session.as_ref() | 129 | self.current_session.as_ref() |
| 84 | } | 130 | } |
| 85 | 131 | ||
| 86 | pub fn get_current_render_lines(&self) -> Option<usize> { | ||
| 87 | let session = self.current_session.as_ref()?; | ||
| 88 | let game = self.games.get(&session.game_type)?; | ||
| 89 | Some(game.render_lines(session)) | ||
| 90 | } | ||
| 91 | |||
| 92 | pub fn is_in_game(&self) -> bool { | 132 | pub fn is_in_game(&self) -> bool { |
| 93 | self.current_session.is_some() | 133 | self.current_session.is_some() |
| 94 | } | 134 | } |
| 95 | 135 | ||
| 96 | pub fn is_game_started(&self) -> bool { | 136 | pub fn end_game(&mut self) { |
| 97 | self.current_session | 137 | self.current_session = None; |
| 98 | .as_ref() | ||
| 99 | .map(|s| s.phase != GamePhase::WaitingToStart) | ||
| 100 | .unwrap_or(false) | ||
| 101 | } | 138 | } |
| 102 | 139 | ||
| 103 | pub fn reset_phase_to_waiting(&mut self) { | 140 | pub fn set_bet(&mut self, bet: u64) -> Result<u64, GameError> { |
| 104 | if let Some(ref mut session) = self.current_session { | 141 | let session = self |
| 105 | session.phase = GamePhase::WaitingToStart; | 142 | .current_session |
| 106 | session.data.clear(); | 143 | .as_mut() |
| 144 | .ok_or(GameError::InvalidAction)?; | ||
| 145 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { | ||
| 146 | return Err(GameError::InvalidAction); | ||
| 107 | } | 147 | } |
| 108 | } | 148 | session.bet_amount = bet.max(10); |
| 109 | 149 | Ok(session.bet_amount) | |
| 110 | pub fn end_game(&mut self) { | ||
| 111 | self.current_session = None; | ||
| 112 | self.pending_game_type = None; | ||
| 113 | } | 150 | } |
| 114 | 151 | ||
| 115 | pub fn adjust_bet(&mut self, delta: i64) -> Result<u64, GameError> { | 152 | pub fn adjust_bet(&mut self, delta: i64) -> Result<u64, GameError> { |
| 116 | if let Some(ref session) = self.current_session { | 153 | let session = self |
| 117 | if session.phase != GamePhase::WaitingToStart { | 154 | .current_session |
| 118 | return Err(GameError::InvalidAction); | 155 | .as_mut() |
| 119 | } | 156 | .ok_or(GameError::InvalidAction)?; |
| 157 | if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { | ||
| 158 | return Err(GameError::InvalidAction); | ||
| 120 | } | 159 | } |
| 121 | 160 | ||
| 122 | if let Some(ref mut session) = self.current_session { | 161 | let current_bet = session.bet_amount as i64; |
| 123 | let current_bet = session.bet_amount as i64; | 162 | session.bet_amount = (current_bet + delta).max(10) as u64; |
| 124 | let new_bet = (current_bet + delta).max(10) as u64; | 163 | Ok(session.bet_amount) |
| 125 | session.bet_amount = new_bet; | ||
| 126 | Ok(new_bet) | ||
| 127 | } else { | ||
| 128 | Err(GameError::InvalidAction) | ||
| 129 | } | ||
| 130 | } | 164 | } |
| 131 | } | 165 | } |
