From 37dedc342c087506e502c78a952b45b1c91602fb Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 18:03:24 +0100 Subject: v4.0.2: port slots, blackjack and crash to structured views --- src/client/games/slots.rs | 125 +++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 69 deletions(-) (limited to 'src/client/games/slots.rs') diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs index 5bffc4c..3aad319 100644 --- a/src/client/games/slots.rs +++ b/src/client/games/slots.rs @@ -1,4 +1,5 @@ -use super::traits::{Game, GamePhase, GameState}; +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. @@ -28,105 +29,91 @@ impl SlotsGame { } } - 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(); + fn render_grid(reels: &[char], winning: &[usize]) -> [String; ROWS] { std::array::from_fn(|row| { (0..REELS) - .map(|col| Self::cell(&chars, row, col, winning)) + .map(|col| Self::cell(reels, row, col, winning)) .collect::>() .join(" ") }) } - fn render_status(&self, state: &GameState) -> String { - if let Some(error) = state.data.get("error") { + fn render_status(view: &SlotsView) -> String { + if let Some(error) = &view.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} Kr"), - Some("big") => format!("BIG WIN! {winnings} Kr"), + + 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"), - }; + } } - return "No win.".to_string(); + SlotsPhase::Ready => "Press [SPACE] to spin!".to_string(), } - "Press [SPACE] to spin!".to_string() } - fn render_win_lines(&self, state: &GameState) -> String { - if state.phase != GamePhase::Finished { + fn render_win_lines(view: &SlotsView) -> String { + if view.phase != SlotsPhase::Result || view.win_lines.is_empty() { 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" + 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(", ")) } - 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); + /// 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", - state.bet_amount, + view.bet, balance, grid[0], grid[1], grid[2], paytable[0], paytable[1], - self.render_status(state), - self.render_win_lines(state) + 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 -- cgit v1.2.3