From 46202ee2e398ffd49febe6684f5c6a8648da600d Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 17:58:03 +0100 Subject: v4.0.1: introduce Command/View and port roulette to the shared boundary --- src/client/games/roulette.rs | 141 ++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 83 deletions(-) (limited to 'src/client/games') diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs index b3a8d9c..ec9573c 100644 --- a/src/client/games/roulette.rs +++ b/src/client/games/roulette.rs @@ -1,4 +1,5 @@ use super::traits::{Game, GameState}; +use crate::client::view::{RoulettePhase, RouletteView}; use crate::roulette::{BetTarget, Bets, Outside, is_even, is_odd}; pub const ROULETTE_BOARD_LINES: usize = 12; @@ -232,22 +233,6 @@ impl RouletteGame { Self } - fn bets(state: &GameState) -> Bets { - state - .data - .get("bets") - .and_then(|raw| Bets::from_json(raw).ok()) - .unwrap_or_default() - } - - fn cursor(state: &GameState) -> Cursor { - state - .data - .get("cursor") - .map(|raw| Cursor::parse(raw)) - .unwrap_or_default() - } - fn zero_cell(cursor: Cursor) -> String { let cell = if cursor == Cursor::Zero { "[ 0 ]".to_string() @@ -329,92 +314,82 @@ impl RouletteGame { format!("Bets: {list} Total: {} Kr", bets.total()) } - fn status_line(state: &GameState) -> String { - if let Some(error) = state.data.get("error") { - if !error.is_empty() { - return format!("ERROR: {error}"); - } + fn status_line(view: &RouletteView) -> String { + if let Some(error) = &view.error { + return format!("ERROR: {error}"); } - match state.data.get("status").map(String::as_str) { - Some("result") => { - let number = state.data.get("number").map(String::as_str).unwrap_or("?"); - let color = state - .data - .get("color") - .map(|value| value.to_uppercase()) + match view.phase { + RoulettePhase::Result => { + let number = view.number.unwrap_or(0); + let color = view + .color + .as_deref() + .map(str::to_uppercase) .unwrap_or_default(); - let parity = number - .parse::() - .ok() - .map(|number| { - if is_even(number) { - " EVEN" - } else if is_odd(number) { - " ODD" - } else { - "" - } - }) - .unwrap_or(""); - let winnings = state - .data - .get("winnings") - .map(String::as_str) - .unwrap_or("0"); - let stake = state.bet_amount; - if winnings == "0" { - format!("No win at {number} {color}{parity}") + let parity = if is_even(number) { + " EVEN" + } else if is_odd(number) { + " ODD" } else { - let profit = winnings.parse::().unwrap_or(0).saturating_sub(stake); - format!("Won {winnings} Kr at {number} {color}{parity} (+{profit})") - } - } - Some("spinning") => "Spinning...".to_string(), - _ => { - let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); - match Self::cursor(state).target() { - Some(target) => format!( - "Bet: {} · pays {} · ENTER places {} Kr", - target.label(), - target.return_label(), - chip - ), - None => "Arrows move, ENTER places the chip, SPACE spins.".to_string(), + "" + }; + match view.winnings.unwrap_or(0) { + 0 => format!("No win at {number} {color}{parity}"), + winnings => { + let profit = winnings.saturating_sub(view.bet_amount); + format!("Won {winnings} Kr at {number} {color}{parity} (+{profit})") + } } } + RoulettePhase::Spinning => "Spinning...".to_string(), + RoulettePhase::Placing => match view.cursor.target() { + Some(target) => format!( + "Bet: {} · pays {} · ENTER places {} Kr", + target.label(), + target.return_label(), + view.chip + ), + None => "Arrows move, ENTER places the chip, SPACE spins.".to_string(), + }, } } -} - -impl Game for RouletteGame { - fn get_name(&self) -> &'static str { - "Roulette" - } - fn render(&self, state: &GameState) -> String { - let bets = Self::bets(state); - let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); - let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); - let cursor = Self::cursor(state); - let covered = cursor.numbers(); + /// Render a roulette view to terminal lines. The GUI draws the same view + /// with a painter instead. + pub fn render_view(view: &RouletteView) -> String { + let covered = view.cursor.numbers(); + let balance = view + .balance + .map(|balance| balance.to_string()) + .unwrap_or_else(|| "?".to_string()); let lines = [ "=== ROULETTE ===".to_string(), - format!("Balance: {balance} Kr Chip: {chip} Kr"), - Self::zero_cell(cursor), + format!("Balance: {balance} Kr Chip: {} Kr", view.chip), + Self::zero_cell(view.cursor), Self::number_row(0, &covered), Self::number_row(1, &covered), Self::number_row(2, &covered), - Self::dozens_row(cursor), - Self::columns_row(cursor), - Self::outside_row(cursor), - Self::bets_line(&bets), - Self::status_line(state), + Self::dozens_row(view.cursor), + Self::columns_row(view.cursor), + Self::outside_row(view.cursor), + Self::bets_line(&view.bets), + Self::status_line(view), "[ARROWS] Move [ENTER] Bet [C]lear [+/-] Chip [SPACE] Spin [Q] Quit".to_string(), ]; lines.join("\r\n") } +} + +impl Game for RouletteGame { + fn get_name(&self) -> &'static str { + "Roulette" + } + + fn render(&self, state: &GameState) -> String { + Self::render_view(&RouletteView::from_state(state)) + } fn render_lines(&self, _state: &GameState) -> usize { ROULETTE_BOARD_LINES -- cgit v1.2.3