use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; use rand::Rng; use std::collections::HashMap; pub struct SlotsGame { symbols: Vec, payouts: HashMap, } impl SlotsGame { pub fn new() -> Self { let symbols = vec!['C', 'L', 'O', 'G', 'D', 'S']; let mut payouts = HashMap::new(); // Balanced payouts: EV = 210/216 ≈ 0.972 (2.8% house edge) payouts.insert("CCC".to_string(), 2); payouts.insert("LLL".to_string(), 3); payouts.insert("OOO".to_string(), 5); payouts.insert("GGG".to_string(), 8); payouts.insert("DDD".to_string(), 14); payouts.insert("SSS".to_string(), 28); payouts.insert("CC".to_string(), 1); payouts.insert("LL".to_string(), 1); payouts.insert("OO".to_string(), 1); payouts.insert("GG".to_string(), 2); payouts.insert("DD".to_string(), 2); payouts.insert("SS".to_string(), 3); Self { symbols, payouts } } fn spin_reels(&self) -> Vec { let mut rng = rand::thread_rng(); (0..3) .map(|_| self.symbols[rng.gen_range(0..self.symbols.len())]) .collect() } pub fn calculate_payout(&self, reels: &[char], bet: u64) -> u64 { let reel_string: String = reels.iter().collect(); if let Some(&multiplier) = self.payouts.get(&reel_string) { return bet * multiplier; } // Check all three pairs including non-adjacent (e.g. [C,D,C]) let pairs: [(usize, usize); 3] = [(0, 1), (1, 2), (0, 2)]; let mut best: u64 = 0; for &(a, b) in &pairs { if reels[a] == reels[b] { let two_of_kind = format!("{}{}", reels[a], reels[a]); if let Some(&multiplier) = self.payouts.get(&two_of_kind) { best = best.max(bet * multiplier); } } } best } } impl Game for SlotsGame { fn game_type(&self) -> &'static str { "slots" } fn get_name(&self) -> &'static str { "Slots" } fn get_description(&self) -> &'static str { "Spin the reels and match symbols to win!" } fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState { let mut data = HashMap::new(); data.insert("username".to_string(), username.to_string()); data.insert("status".to_string(), "ready".to_string()); GameState { session_id: String::new(), game_type: "slots".to_string(), bet_amount, phase: GamePhase::WaitingToStart, data, } } fn process_action( &self, state: &GameState, action: &str, _params: &[&str], ) -> Result { if state.phase == GamePhase::Finished { return Err(GameError::GameOver); } match action { "spin" => { if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing { return Err(GameError::InvalidAction( "Cannot spin in current phase".to_string(), )); } let reels = self.spin_reels(); let reel_string: String = reels.iter().collect(); let mut new_data = state.data.clone(); new_data.insert("reels".to_string(), reel_string.clone()); new_data.insert("status".to_string(), "spun".to_string()); let is_jackpot = reels[0] == reels[1] && reels[1] == reels[2]; let is_two = reels[0] == reels[1] || reels[1] == reels[2] || reels[0] == reels[2]; new_data.insert( "win_type".to_string(), if is_jackpot { "jackpot".to_string() } else if is_two { "two".to_string() } else { "none".to_string() }, ); Ok(GameState { session_id: state.session_id.clone(), game_type: state.game_type.clone(), bet_amount: state.bet_amount, phase: GamePhase::Finished, data: new_data, }) } _ => Err(GameError::InvalidAction(format!( "Unknown action: {}", action ))), } } fn calculate_result(&self, state: &GameState) -> Result { if state.phase != GamePhase::Finished { return Err(GameError::InternalError("Game not finished".to_string())); } let reel_string = state .data .get("reels") .ok_or(GameError::InternalError("No reels found".to_string()))?; let reels: Vec = reel_string.chars().collect(); let winnings = self.calculate_payout(&reels, state.bet_amount); let mut result_data = HashMap::new(); result_data.insert("reels".to_string(), reel_string.clone()); result_data.insert( "payout_multiplier".to_string(), if winnings > 0 && state.bet_amount > 0 { (winnings / state.bet_amount).to_string() } else { "0".to_string() }, ); result_data.insert( "win_type".to_string(), state.data.get("win_type").cloned().unwrap_or_default(), ); Ok(GameResult { session_id: state.session_id.clone(), game_type: state.game_type.clone(), bet_amount: state.bet_amount, winnings, final_balance: 0, result_data, }) } }