use super::traits::{Game, GamePhase, GameState}; /// Height of the game board. It must never depend on the spin result. pub const SLOT_BOARD_LINES: usize = 9; pub const LINES: usize = 5; pub const REELS: usize = 5; pub const ROWS: usize = 3; pub struct SlotsGame; impl SlotsGame { pub fn new() -> Self { Self } fn cell(chars: &[char], row: usize, col: usize, winning: &[usize]) -> String { let symbol = if chars.len() == REELS * ROWS { chars.get(col * ROWS + row).copied().unwrap_or('?') } else if chars.len() == 3 && row == 0 { // Legacy 3-symbol server: degrade to the first row. chars.get(col).copied().unwrap_or('?') } else { '?' }; if winning.contains(&(col * ROWS + row)) { format!("{{ {} }}", symbol) } else { format!("[ {} ]", symbol) } } fn highlighted_cells(state: &GameState) -> Vec { state .data .get("win_cells") .map(|raw| { raw.split(',') .filter_map(|value| value.trim().parse::().ok()) .collect() }) .unwrap_or_default() } fn render_grid(&self, reels: &str, winning: &[usize]) -> [String; ROWS] { let chars: Vec = reels.chars().collect(); std::array::from_fn(|row| { (0..REELS) .map(|col| Self::cell(&chars, row, col, winning)) .collect::>() .join(" ") }) } fn render_status(&self, state: &GameState) -> String { if let Some(error) = state.data.get("error") { return format!("ERROR: {error}"); } if state.phase == GamePhase::Playing { return "Spinning...".to_string(); } if state.phase == GamePhase::Finished { let winnings = state .data .get("winnings") .map(String::as_str) .unwrap_or("0"); if winnings != "0" { return match state.data.get("win_type").map(String::as_str) { Some("mega") => format!("MEGA WIN! {winnings} PLN"), Some("big") => format!("BIG WIN! {winnings} PLN"), _ => format!("Won {winnings} PLN"), }; } return "No win.".to_string(); } "Press [SPACE] to spin!".to_string() } fn render_win_lines(&self, state: &GameState) -> String { if state.phase != GamePhase::Finished { return String::new(); } match state.data.get("win_lines").map(String::as_str) { Some(lines) if !lines.is_empty() && lines != "-" => { format!("Winning lines: {lines}") } _ => String::new(), } } } impl Game for SlotsGame { fn get_name(&self) -> &'static str { "Slots" } fn render(&self, state: &GameState) -> String { let reels = state.data.get("reels").cloned().unwrap_or_default(); let balance = state .data .get("balance") .map(String::as_str) .unwrap_or("?"); let winning = Self::highlighted_cells(state); let grid = self.render_grid(&reels, &winning); format!( "=== SLOTS ===\r\nBet: {} PLN Lines: {} Balance: {} PLN\r\n{}\r\n{}\r\n{}\r\n\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit", state.bet_amount, LINES, balance, grid[0], grid[1], grid[2], self.render_status(state), self.render_win_lines(state) ) } fn render_lines(&self, _state: &GameState) -> usize { SLOT_BOARD_LINES } fn get_help(&self) -> String { "SLOTS GAME\n\ =========\n\ 5 reels x 3 rows, 5 paylines.\n\ Match 3, 4 or 5 symbols from the left on a payline.\n\ \n\ Payouts (x total bet, 3/4/5 of a kind):\n\ C: 2 / 5 / 15\n\ L: 3 / 7 / 20\n\ O: 3 / 8 / 25\n\ G: 4 / 12 / 40\n\ D: 6 / 18 / 70\n\ S: 9 / 30 / 150\n\ \n\ Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ \n\ Controls:\n\ SPACE - Spin the reels\n\ +/- - Adjust total bet\n\ Q - Exit the game" .to_string() } }