use super::traits::{Game, GameState}; use crate::client::view::{SlotsPhase, SlotsView}; use crate::slots::{LINE_NAMES, REELS, ROWS, paytable_lines}; /// 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 render_grid(reels: &[char], winning: &[usize]) -> [String; ROWS] { std::array::from_fn(|row| { (0..REELS) .map(|col| Self::cell(reels, row, col, winning)) .collect::>() .join(" ") }) } fn render_status(view: &SlotsView) -> String { if let Some(error) = &view.error { return format!("ERROR: {error}"); } match view.phase { SlotsPhase::Spinning => "Spinning...".to_string(), SlotsPhase::Result => { let winnings = view.winnings.unwrap_or(0); if winnings == 0 { return "No win.".to_string(); } match view.win_type.as_str() { "mega" => format!("MEGA WIN! {winnings} Kr"), "big" => format!("BIG WIN! {winnings} Kr"), _ => format!("Won {winnings} Kr"), } } SlotsPhase::Ready => "Press [SPACE] to spin!".to_string(), } } fn render_win_lines(view: &SlotsView) -> String { if view.phase != SlotsPhase::Result || view.win_lines.is_empty() { return String::new(); } let named: Vec = view .win_lines .iter() .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(", ")) } /// Render a slots view to terminal lines. The GUI paints the same view. pub fn render_view(view: &SlotsView) -> String { let balance = view .balance .map(|balance| balance.to_string()) .unwrap_or_else(|| "?".to_string()); let grid = Self::render_grid(&view.reels, &view.win_cells); let paytable = paytable_lines(); format!( "=== SLOTS ===\r\nBet: {} Kr Balance: {} Kr\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit", view.bet, balance, grid[0], grid[1], grid[2], paytable[0], paytable[1], Self::render_status(view), Self::render_win_lines(view) ) } } impl Game for SlotsGame { fn get_name(&self) -> &'static str { "Slots" } fn render(&self, state: &GameState) -> String { Self::render_view(&SlotsView::from_state(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() } }