From ea5e7d336719571e51ef14478ab603e71126e3d8 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 22:28:05 +0100 Subject: v0.2.2: split client/server logic into the library --- src/server/games/slots.rs | 179 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/server/games/slots.rs (limited to 'src/server/games/slots.rs') diff --git a/src/server/games/slots.rs b/src/server/games/slots.rs new file mode 100644 index 0000000..0a9797d --- /dev/null +++ b/src/server/games/slots.rs @@ -0,0 +1,179 @@ +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, + }) + } +} -- cgit v1.2.3