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/blackjack.rs | 59 +++++++++----------- src/client/games/crash.rs | 67 +++++++++++----------- src/client/games/slots.rs | 125 +++++++++++++++++++----------------------- 3 files changed, 116 insertions(+), 135 deletions(-) (limited to 'src/client/games') diff --git a/src/client/games/blackjack.rs b/src/client/games/blackjack.rs index 8fa5693..e8f251f 100644 --- a/src/client/games/blackjack.rs +++ b/src/client/games/blackjack.rs @@ -1,5 +1,6 @@ use super::traits::{Game, GameState}; -use crate::blackjack::{BlackjackState, card_label}; +use crate::blackjack::card_label; +use crate::client::view::BlackjackView; pub const BLACKJACK_BOARD_LINES: usize = 6; @@ -10,13 +11,6 @@ impl BlackjackGame { Self } - fn state(state: &GameState) -> Option { - state - .data - .get("blackjack") - .and_then(|raw| serde_json::from_str(raw).ok()) - } - fn cards(cards: &[String]) -> String { cards .iter() @@ -43,40 +37,29 @@ impl BlackjackGame { _ => "Hand complete.".to_string(), } } -} -impl Game for BlackjackGame { - fn get_name(&self) -> &'static str { - "Blackjack" - } - - fn render(&self, state: &GameState) -> String { - let status = state - .data - .get("status") - .map(String::as_str) - .unwrap_or("ready"); - let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); - let hand = Self::state(state); + /// Render a blackjack view to terminal lines. The GUI paints the same view. + pub fn render_view(view: &BlackjackView) -> String { + let balance = view + .balance + .map(|balance| balance.to_string()) + .unwrap_or_else(|| "?".to_string()); - if let Some(error) = state.data.get("error") { - let bet = hand - .as_ref() - .map(|hand| hand.bet) - .unwrap_or(state.bet_amount); + if let Some(error) = &view.error { + let bet = view.hand.as_ref().map(|hand| hand.bet).unwrap_or(view.bet); return format!( "=== BLACKJACK ===\r\nBalance: {balance} Kr Bet: {bet} Kr\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit" ); } let (dealer, dealer_total, player, player_total, bet, text, controls) = - if let Some(hand) = &hand { - let bet = if status == "player" { + if let Some(hand) = &view.hand { + let bet = if view.status == "player" { hand.bet } else { - state.bet_amount + view.bet }; - let text = match status { + let text = match view.status.as_str() { "player" => { if hand.actions.iter().any(|action| action == "double") { "Hit, stand or double.".to_string() @@ -89,7 +72,7 @@ impl Game for BlackjackGame { "done" => Self::result_text(&hand.result, hand.winnings), _ => "Press [SPACE] to deal.".to_string(), }; - let controls = if status == "player" { + let controls = if view.status == "player" { if hand.actions.iter().any(|action| action == "double") { "[H]it [S]tand [D]ouble [Q] Forfeit".to_string() } else { @@ -113,7 +96,7 @@ impl Game for BlackjackGame { 0, "—".to_string(), 0, - state.bet_amount, + view.bet, "Press [SPACE] to deal.".to_string(), "[SPACE] Deal [+/-] Bet [Q] Quit".to_string(), ) @@ -124,6 +107,16 @@ impl Game for BlackjackGame { balance, bet, dealer, dealer_total, player, player_total, text, controls ) } +} + +impl Game for BlackjackGame { + fn get_name(&self) -> &'static str { + "Blackjack" + } + + fn render(&self, state: &GameState) -> String { + Self::render_view(&BlackjackView::from_state(state)) + } fn render_lines(&self, _state: &GameState) -> usize { BLACKJACK_BOARD_LINES diff --git a/src/client/games/crash.rs b/src/client/games/crash.rs index 383e807..affb642 100644 --- a/src/client/games/crash.rs +++ b/src/client/games/crash.rs @@ -1,4 +1,5 @@ use super::traits::{Game, GameState}; +use crate::client::view::CrashView; pub const CRASH_BOARD_LINES: usize = 6; @@ -9,67 +10,67 @@ impl CrashGame { Self } - fn value<'a>(state: &'a GameState, key: &str) -> Option<&'a str> { - state.data.get(key).map(String::as_str) - } - - fn status_text(state: &GameState) -> String { - let status = Self::value(state, "status").unwrap_or("ready"); - match status { + fn status_text(view: &CrashView) -> String { + match view.status.as_str() { "starting" => "Starting...".to_string(), "running" => "Rising... cash out before it crashes.".to_string(), "cashed" => format!( - "Cashed out at {}x Won {} Kr", - Self::value(state, "multiplier").unwrap_or("1.00"), - Self::value(state, "payout").unwrap_or("0") + "Cashed out at {:.2}x Won {} Kr", + view.multiplier, view.payout ), "crashed" => format!( "CRASHED at {}x Bet lost", - Self::value(state, "crash_point").unwrap_or("?") + view.crash_point + .map(|point| format!("{point:.2}")) + .unwrap_or_else(|| "?".to_string()) ), "forfeit" => "Forfeited. Bet lost.".to_string(), _ => "Press [SPACE] to start.".to_string(), } } - fn controls(state: &GameState) -> &'static str { - if Self::value(state, "status") == Some("running") { + fn controls(view: &CrashView) -> &'static str { + if view.status == "running" { "[C] Cash out [Q] Forfeit" } else { "[SPACE] Play [+/=/-] Bet [Q] Quit" } } -} -impl Game for CrashGame { - fn get_name(&self) -> &'static str { - "Crash" - } - - fn render(&self, state: &GameState) -> String { - let balance = Self::value(state, "balance").unwrap_or("?"); - let multiplier = Self::value(state, "multiplier").unwrap_or("1.00"); - let potential = Self::value(state, "potential") - .map(str::to_string) - .unwrap_or_else(|| state.bet_amount.to_string()); + /// Render a crash view to terminal lines. The GUI paints the same view. + pub fn render_view(view: &CrashView) -> String { + let balance = view + .balance + .map(|balance| balance.to_string()) + .unwrap_or_else(|| "?".to_string()); - if let Some(error) = Self::value(state, "error") { + if let Some(error) = &view.error { return format!( "=== CRASH ===\r\nBalance: {balance} Kr Bet: {} Kr\r\n\r\nERROR: {error}\r\n\r\n[SPACE] Play [+/=/-] Bet [Q] Quit", - state.bet_amount + view.bet ); } format!( - "=== CRASH ===\r\nBalance: {} Kr Bet: {} Kr\r\nMultiplier: {}x\r\nPotential: {} Kr\r\n{}\r\n{}", + "=== CRASH ===\r\nBalance: {} Kr Bet: {} Kr\r\nMultiplier: {:.2}x\r\nPotential: {} Kr\r\n{}\r\n{}", balance, - state.bet_amount, - multiplier, - potential, - Self::status_text(state), - Self::controls(state) + view.bet, + view.multiplier, + view.potential, + Self::status_text(view), + Self::controls(view) ) } +} + +impl Game for CrashGame { + fn get_name(&self) -> &'static str { + "Crash" + } + + fn render(&self, state: &GameState) -> String { + Self::render_view(&CrashView::from_state(state)) + } fn render_lines(&self, _state: &GameState) -> usize { CRASH_BOARD_LINES 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