use super::blackjack::BlackjackGame; use super::crash::CrashGame; use super::response::{RouletteResult, SpinResult}; use super::roulette::RouletteGame; use super::slots::SlotsGame; use super::traits::{Game, GameError, GamePhase, GameState}; use crate::blackjack::BlackjackState; use crate::crash::{payout, CrashState}; use crate::roulette::Bets; use std::collections::HashMap; pub struct GameManager { current_session: Option, games: HashMap>, } impl GameManager { pub fn new() -> Self { let mut games: HashMap> = HashMap::new(); games.insert("slots".to_string(), Box::new(SlotsGame::new())); games.insert("roulette".to_string(), Box::new(RouletteGame::new())); games.insert("blackjack".to_string(), Box::new(BlackjackGame::new())); games.insert("crash".to_string(), Box::new(CrashGame::new())); Self { current_session: None, games, } } pub fn get_available_games(&self) -> Vec<(&str, &str)> { let mut games: Vec<(&str, &str)> = self .games .iter() .map(|(key, game)| (key.as_str(), game.get_name())) .collect(); games.sort_unstable_by_key(|(key, _)| *key); games } pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> { if self.current_session.is_some() { return Err(GameError::InvalidAction); } if !self.games.contains_key(game_type) { return Err(GameError::InvalidAction); } let mut data = HashMap::new(); let bet_amount = if game_type == "roulette" { data.insert( "bets".to_string(), serde_json::to_string(&Bets::default()).unwrap_or_else(|_| "{}".to_string()), ); data.insert("chip".to_string(), "10".to_string()); data.insert("status".to_string(), "place".to_string()); data.insert("cursor".to_string(), "lattice:0:3".to_string()); 0 } else if game_type == "blackjack" { data.insert("status".to_string(), "ready".to_string()); 10 } else if game_type == "crash" { data.insert("status".to_string(), "ready".to_string()); data.insert("multiplier".to_string(), "1.00".to_string()); 10 } else { 10 }; self.current_session = Some(GameState { game_type: game_type.to_string(), bet_amount, phase: GamePhase::WaitingToStart, data, }); Ok(()) } /// The wire request for a spin, if the current phase allows one. pub fn spin_request(&self) -> Option { let session = self.current_session.as_ref()?; if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { return None; } if session.game_type == "roulette" { let bets = session .data .get("bets") .cloned() .unwrap_or_else(|| "{}".to_string()); return Some(format!( "PLAY_START roulette {} {}", session.bet_amount, bets )); } Some(format!( "PLAY_START {} {}", session.game_type, session.bet_amount )) } /// Move the board into its spinning state and drop the previous result /// so stale reels/winnings cannot leak into the new spin. pub fn begin_spin(&mut self) -> Result<(), GameError> { let session = self .current_session .as_mut() .ok_or(GameError::InvalidAction)?; if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { return Err(GameError::InvalidAction); } session.phase = GamePhase::Playing; session.data.remove("error"); session.data.remove("reels"); session.data.remove("win_type"); session.data.remove("win_lines"); session.data.remove("win_cells"); session.data.remove("winnings"); session.data.remove("number"); session.data.remove("color"); if session.game_type == "roulette" { session .data .insert("status".to_string(), "spinning".to_string()); } Ok(()) } /// Store the settled spin so `render()` can draw it in place. pub fn apply_result(&mut self, result: SpinResult) { let Some(session) = self.current_session.as_mut() else { return; }; session.phase = GamePhase::Finished; session.bet_amount = result.bet_amount; session.data.remove("error"); session.data.insert("reels".to_string(), result.reels); session.data.insert("win_type".to_string(), result.win_type); session.data.insert( "win_lines".to_string(), result .win_lines .iter() .map(usize::to_string) .collect::>() .join(","), ); session.data.insert( "win_cells".to_string(), result .win_cells .iter() .map(usize::to_string) .collect::>() .join(","), ); session .data .insert("winnings".to_string(), result.winnings.to_string()); session .data .insert("balance".to_string(), result.balance.to_string()); } /// Show a failed spin without leaving the player stuck in `Playing`. pub fn set_error(&mut self, message: impl Into) { let Some(session) = self.current_session.as_mut() else { return; }; session.phase = GamePhase::WaitingToStart; session.data.remove("reels"); session.data.remove("win_type"); session.data.remove("win_lines"); session.data.remove("win_cells"); session.data.remove("winnings"); session.data.remove("number"); session.data.remove("color"); session.data.remove("payout"); session.data.remove("crash_point"); if session.game_type == "roulette" { session .data .insert("status".to_string(), "place".to_string()); } if session.game_type == "crash" { session .data .insert("status".to_string(), "ready".to_string()); session.data.remove("session_id"); } session.data.insert("error".to_string(), message.into()); } pub fn set_balance(&mut self, balance: i64) { if let Some(session) = self.current_session.as_mut() { session .data .insert("balance".to_string(), balance.to_string()); } } pub fn roulette_bets(&self) -> Bets { self.current_session .as_ref() .and_then(|session| session.data.get("bets")) .and_then(|raw| serde_json::from_str(raw).ok()) .unwrap_or_default() } pub fn set_roulette_bets(&mut self, bets: &Bets) { if let Some(session) = self.current_session.as_mut() { session.data.insert( "bets".to_string(), serde_json::to_string(bets).unwrap_or_else(|_| "{}".to_string()), ); session.bet_amount = bets.total(); } } pub fn roulette_chip(&self) -> u64 { self.current_session .as_ref() .and_then(|session| session.data.get("chip")) .and_then(|value| value.parse().ok()) .unwrap_or(10) } pub fn set_roulette_chip(&mut self, chip: u64) { if let Some(session) = self.current_session.as_mut() { session.data.insert("chip".to_string(), chip.to_string()); } } pub fn roulette_cursor(&self) -> crate::client::games::roulette::Cursor { self.current_session .as_ref() .and_then(|session| session.data.get("cursor")) .map(|raw| crate::client::games::roulette::Cursor::parse(raw)) .unwrap_or_default() } pub fn set_roulette_cursor(&mut self, cursor: &crate::client::games::roulette::Cursor) { if let Some(session) = self.current_session.as_mut() { session .data .insert("cursor".to_string(), cursor.encoded()); } } pub fn set_roulette_status(&mut self, status: &str) { if let Some(session) = self.current_session.as_mut() { session .data .insert("status".to_string(), status.to_string()); if status != "error" { session.data.remove("error"); } } } pub fn apply_roulette_result(&mut self, result: RouletteResult) { let Some(session) = self.current_session.as_mut() else { return; }; session.phase = GamePhase::Finished; session.bet_amount = result.bet_amount; session.data.remove("error"); session.data.insert("bets".to_string(), result.bets); session .data .insert("number".to_string(), result.number.to_string()); session.data.insert("color".to_string(), result.color); session .data .insert("winnings".to_string(), result.winnings.to_string()); session .data .insert("balance".to_string(), result.balance.to_string()); session .data .insert("status".to_string(), "result".to_string()); } pub fn apply_blackjack_state(&mut self, state: BlackjackState) { let Some(session) = self.current_session.as_mut() else { return; }; session.phase = if state.phase == "player" { GamePhase::Playing } else { GamePhase::Finished }; session.bet_amount = state.base_bet; session.data.remove("error"); session .data .insert("balance".to_string(), state.balance.to_string()); session.data.insert( "status".to_string(), if state.phase == "player" { "player".to_string() } else { "done".to_string() }, ); session.data.insert( "blackjack".to_string(), serde_json::to_string(&state).unwrap_or_default(), ); } pub fn blackjack_state(&self) -> Option { self.current_session .as_ref() .and_then(|session| session.data.get("blackjack")) .and_then(|raw| serde_json::from_str(raw).ok()) } pub fn set_blackjack_status(&mut self, status: &str) { if let Some(session) = self.current_session.as_mut() { session .data .insert("status".to_string(), status.to_string()); } } pub fn apply_crash_state(&mut self, state: CrashState) { let Some(session) = self.current_session.as_mut() else { return; }; let running = state.is_running(); session.phase = if running { GamePhase::Playing } else { GamePhase::Finished }; session.bet_amount = state.bet; session.data.remove("error"); for key in ["crash_point", "payout"] { session.data.remove(key); } session .data .insert("status".to_string(), state.phase.clone()); session .data .insert("session_id".to_string(), state.session_id); session .data .insert("balance".to_string(), state.balance.to_string()); session .data .insert("multiplier".to_string(), state.multiplier.to_string()); session .data .insert("potential".to_string(), state.potential.to_string()); session .data .insert("payout".to_string(), state.payout.to_string()); if let Some(crash_point) = state.crash_point { session .data .insert("crash_point".to_string(), format!("{crash_point:.2}")); } } /// Redraw the live multiplier from the client's local clock. pub fn set_crash_multiplier(&mut self, multiplier: f64) { let Some(session) = self.current_session.as_mut() else { return; }; let bet = session.bet_amount; // Keep the full value: the terminal rounds for display, but the GUI // charts it and must not draw a staircase. session .data .insert("multiplier".to_string(), multiplier.to_string()); session .data .insert("potential".to_string(), payout(bet, multiplier).to_string()); } pub fn set_crash_status(&mut self, status: &str) { if let Some(session) = self.current_session.as_mut() { session .data .insert("status".to_string(), status.to_string()); } } pub fn crash_status(&self) -> String { self.current_session .as_ref() .and_then(|session| session.data.get("status")) .cloned() .unwrap_or_else(|| "ready".to_string()) } pub fn crash_session_id(&self) -> Option { self.current_session .as_ref() .and_then(|session| session.data.get("session_id")) .cloned() } pub fn get_current_game(&self) -> Option<&dyn Game> { self.current_session .as_ref() .and_then(|session| self.games.get(&session.game_type).map(|game| game.as_ref())) } pub fn get_current_state(&self) -> Option<&GameState> { self.current_session.as_ref() } pub fn is_in_game(&self) -> bool { self.current_session.is_some() } pub fn end_game(&mut self) { self.current_session = None; } pub fn set_bet(&mut self, bet: u64) -> Result { let session = self .current_session .as_mut() .ok_or(GameError::InvalidAction)?; if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { return Err(GameError::InvalidAction); } if session.game_type == "roulette" { session .data .insert("chip".to_string(), bet.max(10).to_string()); return Ok(bet.max(10)); } session.bet_amount = bet.max(10); Ok(session.bet_amount) } pub fn adjust_bet(&mut self, delta: i64) -> Result { let session = self .current_session .as_mut() .ok_or(GameError::InvalidAction)?; if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { return Err(GameError::InvalidAction); } let current_bet = session.bet_amount as i64; session.bet_amount = (current_bet + delta).max(10) as u64; Ok(session.bet_amount) } }