use super::traits::{Game, GamePhase, GameState}; use crate::slots::{paytable_lines, LINE_NAMES, REELS, ROWS}; /// Height of the game board. It must never depend on the spin result. pub const SLOT_BOARD_LINES: usize = 10; 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 != "-" => { let named: Vec = lines .split(',') .filter_map(|raw| raw.trim().parse::().ok()) .map(|line| { let name = line .checked_sub(1) .and_then(|index| LINE_NAMES.get(index)) .copied(); match name { Some(name) => format!("{line} ({name})"), None => line.to_string(), } }) .collect(); format!("Winning lines: {}", named.join(", ")) } _ => 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); let paytable = paytable_lines(); format!( "=== SLOTS ===\r\nBet: {} PLN Balance: {} PLN\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit", state.bet_amount, balance, grid[0], grid[1], grid[2], paytable[0], paytable[1], 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\ A win needs 3, 4 or 5 matching symbols from reel 1 on a payline.\n\ \n\ Payout = total bet x symbol x shape\n\ Symbols: C=1 L=2 O=3 G=4 D=6 S=10\n\ Shapes: 3 of a kind=1, 4 of a kind=3, 5 of a kind=14\n\ \n\ Lines: 1 top, 2 middle, 3 bottom, 4 V, 5 inverted V\n\ Multiple winning lines are added together.\n\ \n\ Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ \n\ Controls:\n\ SPACE - Spin the reels\n\ + or = - Increase total bet\n\ - - Decrease total bet\n\ Q - Exit the game" .to_string() } }