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/core.rs | 232 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 134 insertions(+), 98 deletions(-) (limited to 'src/client/core.rs') diff --git a/src/client/core.rs b/src/client/core.rs index 7e43fb7..3e3126a 100644 --- a/src/client/core.rs +++ b/src/client/core.rs @@ -4,7 +4,7 @@ use crate::client::games::response::{ parse_roulette_response, parse_spin_response, }; use crate::client::games::roulette::Direction; -use crate::client::view::{Command, RouletteView, View}; +use crate::client::view::{BlackjackView, Command, CrashView, RouletteView, SlotsView, View}; use crate::client::ui::{ command_names, completion_lines, display_width, help_lines, history_lines, input_cursor_column, parse_bet_arg, record_history, @@ -38,7 +38,7 @@ pub enum InteractiveCommand { /// High-level actions produced by the client core. The terminal adapter is /// responsible for turning these into crossterm calls and network writes. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum Effect { /// Append lines to the scrolling transcript. Print(Vec), @@ -213,7 +213,9 @@ impl ClientCore { self.balance = Some(result.balance); let winning = result.number; self.game.apply_roulette_result(result); - let view = self.view(); + let Some(view) = self.view() else { + return Vec::new(); + }; vec![Effect::AnimateRoulette { winning, view }] } Err(message) => { @@ -340,22 +342,10 @@ impl ClientCore { match key { Key::CtrlC => vec![Effect::Quit], - Key::Esc | Key::Char('q') => { - self.game.end_game(); - vec![ - Effect::ClearRegion, - Effect::Print(vec!["Game exited.".to_string()]), - ] - } - Key::Char(' ') => self.spin(), - Key::Char('+') | Key::Char('=') => { - let _ = self.game.adjust_bet(10); - self.board_effects() - } - Key::Char('-') => { - let _ = self.game.adjust_bet(-10); - self.board_effects() - } + Key::Esc | Key::Char('q') => self.command(Command::Leave), + Key::Char(' ') => self.command(Command::Spin), + Key::Char('+') | Key::Char('=') => self.command(Command::AdjustBet(10)), + Key::Char('-') => self.command(Command::AdjustBet(-10)), _ => Vec::new(), } } @@ -393,7 +383,46 @@ impl ClientCore { /// Apply a semantic command. Both frontends drive the core through this. pub fn command(&mut self, command: Command) -> Vec { + let game = self.current_game_type().map(str::to_string); + match command { + Command::SetBet(bet) => { + if game.as_deref() == Some("roulette") { + return self.command(Command::SetChip(bet)); + } + if self.game.set_bet(bet).is_ok() { + self.board_effects() + } else { + Vec::new() + } + } + Command::AdjustBet(delta) => { + if game.as_deref() == Some("roulette") { + return self.command(Command::AdjustChip(delta)); + } + if self.game.adjust_bet(delta).is_ok() { + self.board_effects() + } else { + Vec::new() + } + } + Command::Leave => self.leave_current_game(), + Command::Spin => match game.as_deref() { + Some("slots") => self.spin(), + Some("roulette") => self.spin_roulette(), + _ => Vec::new(), + }, + Command::Deal => self.start_blackjack(), + Command::Hit => self.blackjack_action("hit"), + Command::Stand => self.blackjack_action("stand"), + Command::Double => self.blackjack_action("double"), + Command::Start => self.start_crash(), + Command::CashOut => self.cash_out_crash(), + Command::Forfeit => match game.as_deref() { + Some("blackjack") => self.forfeit_blackjack(), + Some("crash") => self.forfeit_crash(), + _ => Vec::new(), + }, Command::MoveCursor(direction) => { let cursor = self.game.roulette_cursor().moved(direction); self.game.set_roulette_cursor(&cursor); @@ -418,16 +447,67 @@ impl ClientCore { self.game.set_roulette_chip(chip); self.board_effects() } - Command::Spin => self.spin_roulette(), - Command::Leave => { - self.crash_started = None; - self.game.end_game(); - vec![ - Effect::ClearRegion, - Effect::Print(vec!["Game exited.".to_string()]), - ] + } + } + + fn current_game_type(&self) -> Option<&str> { + self.game + .get_current_state() + .map(|state| state.game_type.as_str()) + } + + fn leave_current_game(&mut self) -> Vec { + match self.current_game_type().map(str::to_string).as_deref() { + Some("blackjack") => { + if self + .game + .blackjack_state() + .is_some_and(|hand| hand.phase == "player") + { + return self.forfeit_blackjack(); + } } + Some("crash") => { + if self.game.crash_status() == "running" { + return self.forfeit_crash(); + } + } + _ => {} } + self.exit_game() + } + + fn exit_game(&mut self) -> Vec { + self.crash_started = None; + self.game.end_game(); + vec![ + Effect::ClearRegion, + Effect::Print(vec!["Game exited.".to_string()]), + ] + } + + fn forfeit_blackjack(&mut self) -> Vec { + let Some(hand) = self + .game + .blackjack_state() + .filter(|hand| hand.phase == "player") + else { + return self.exit_game(); + }; + self.game.set_blackjack_status("forfeiting"); + self.pending = Some(Pending::BlackjackForfeit); + vec![Effect::SendLine(format!( + "BLACKJACK_ACTION {} forfeit", + hand.session_id + ))] + } + + fn forfeit_crash(&mut self) -> Vec { + let Some(session) = self.game.crash_session_id() else { + return self.exit_game(); + }; + self.pending = Some(Pending::CrashForfeit); + vec![Effect::SendLine(format!("CRASH_FORFEIT {session}"))] } fn place_roulette_chip_on(&mut self, target: BetTarget) -> Vec { @@ -482,46 +562,23 @@ impl ClientCore { .cloned() .unwrap_or_else(|| "ready".to_string()); let hand = self.game.blackjack_state(); + let player = hand.as_ref().is_some_and(|hand| hand.phase == "player"); + let can_double = hand.as_ref().is_some_and(|hand| { + hand.phase == "player" && hand.actions.iter().any(|action| action == "double") + }); match key { Key::CtrlC => vec![Effect::Quit], - Key::Char('q') => { - if let Some(hand) = hand.filter(|hand| hand.phase == "player") { - self.game.set_blackjack_status("forfeiting"); - self.pending = Some(Pending::BlackjackForfeit); - vec![Effect::SendLine(format!( - "BLACKJACK_ACTION {} forfeit", - hand.session_id - ))] - } else { - self.game.end_game(); - vec![ - Effect::ClearRegion, - Effect::Print(vec!["Game exited.".to_string()]), - ] - } - } - Key::Char(' ') => self.start_blackjack(), - Key::Char('h') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => { - self.blackjack_action("hit") - } - Key::Char('s') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => { - self.blackjack_action("stand") - } - Key::Char('d') - if hand.as_ref().is_some_and(|hand| { - hand.phase == "player" && hand.actions.iter().any(|action| action == "double") - }) => - { - self.blackjack_action("double") - } + Key::Char('q') => self.command(Command::Leave), + Key::Char(' ') => self.command(Command::Deal), + Key::Char('h') if player => self.command(Command::Hit), + Key::Char('s') if player => self.command(Command::Stand), + Key::Char('d') if can_double => self.command(Command::Double), Key::Char('+') | Key::Char('=') if matches!(status.as_str(), "ready" | "done") => { - let _ = self.game.adjust_bet(10); - self.board_effects() + self.command(Command::AdjustBet(10)) } Key::Char('-') if matches!(status.as_str(), "ready" | "done") => { - let _ = self.game.adjust_bet(-10); - self.board_effects() + self.command(Command::AdjustBet(-10)) } _ => Vec::new(), } @@ -643,32 +700,15 @@ impl ClientCore { match key { Key::CtrlC => vec![Effect::Quit], - Key::Char('q') => { - if running { - let Some(session) = self.game.crash_session_id() else { - return Vec::new(); - }; - self.pending = Some(Pending::CrashForfeit); - vec![Effect::SendLine(format!("CRASH_FORFEIT {session}"))] - } else { - self.crash_started = None; - self.game.end_game(); - vec![ - Effect::ClearRegion, - Effect::Print(vec!["Game exited.".to_string()]), - ] - } + Key::Char('q') => self.command(Command::Leave), + Key::Char(' ') if !running && self.pending.is_none() => self.command(Command::Start), + Key::Char('c') if running && self.pending.is_none() => { + self.command(Command::CashOut) } - Key::Char(' ') if !running && self.pending.is_none() => self.start_crash(), - Key::Char('c') if running && self.pending.is_none() => self.cash_out_crash(), Key::Char('+') | Key::Char('=') if !running => { - let _ = self.game.adjust_bet(10); - self.board_effects() - } - Key::Char('-') if !running => { - let _ = self.game.adjust_bet(-10); - self.board_effects() + self.command(Command::AdjustBet(10)) } + Key::Char('-') if !running => self.command(Command::AdjustBet(-10)), _ => Vec::new(), } } @@ -948,24 +988,20 @@ impl ClientCore { effects } - fn board_lines(&self) -> Option> { - let game = self.game.get_current_game()?; - let state = self.game.get_current_state()?; - Some(game.render(state).lines().map(str::to_string).collect()) - } - /// Snapshot of the current screen for a frontend to draw. - pub fn view(&self) -> View { - if let Some(state) = self.game.get_current_state() { - if state.game_type == "roulette" { - return View::Roulette(RouletteView::from_state(state)); - } - } - View::Text(self.board_lines().unwrap_or_default()) + pub fn view(&self) -> Option { + let state = self.game.get_current_state()?; + Some(match state.game_type.as_str() { + "slots" => View::Slots(SlotsView::from_state(state)), + "roulette" => View::Roulette(RouletteView::from_state(state)), + "blackjack" => View::Blackjack(BlackjackView::from_state(state)), + "crash" => View::Crash(CrashView::from_state(state)), + _ => return None, + }) } fn board_effects(&self) -> Vec { - vec![Effect::Render(self.view())] + self.view().map(Effect::Render).into_iter().collect() } fn prompt_effects(&self) -> Vec { -- cgit v1.2.3