From 50fdaa8e5ad9454e783f01e2c369767834120d11 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 18:08:38 +0100 Subject: v4.0.3: lobby and history screens behind commands --- src/client/core.rs | 151 ++++++++++++++++++++++++++++++++++++++++------------- src/client/tui.rs | 2 + src/client/view.rs | 21 ++++++++ 3 files changed, 137 insertions(+), 37 deletions(-) (limited to 'src/client') diff --git a/src/client/core.rs b/src/client/core.rs index 3e3126a..37ba916 100644 --- a/src/client/core.rs +++ b/src/client/core.rs @@ -4,13 +4,15 @@ use crate::client::games::response::{ parse_roulette_response, parse_spin_response, }; use crate::client::games::roulette::Direction; -use crate::client::view::{BlackjackView, Command, CrashView, RouletteView, SlotsView, View}; +use crate::client::view::{ + BlackjackView, Command, CrashView, HistoryView, LobbyView, 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, }; use crate::crash::{CrashState, multiplier_at}; -use crate::history::MAX_HISTORY; +use crate::history::{PlayRecord, MAX_HISTORY}; use crate::roulette::{BetTarget, Bets}; use std::time::{Duration, Instant}; @@ -80,6 +82,12 @@ enum Pending { Info, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Screen { + Lobby, + History, +} + pub struct ClientCore { input: String, history: Vec, @@ -93,6 +101,10 @@ pub struct ClientCore { fingerprint: Option, /// Local anchor for the crash multiplier, set when a round starts. crash_started: Option, + /// App screen shown when no game is open. + screen: Screen, + /// Last plays fetched with `history`. + play_history: Vec, } impl ClientCore { @@ -109,6 +121,8 @@ impl ClientCore { host: None, fingerprint: None, crash_started: None, + screen: Screen::Lobby, + play_history: Vec::new(), } } @@ -118,6 +132,7 @@ impl ClientCore { self.logged_in = None; self.pending = None; self.crash_started = None; + self.screen = Screen::Lobby; } } @@ -291,7 +306,10 @@ impl ClientCore { } }, Some(Pending::History) => match parse_history_response(reply) { - Ok(records) => vec![Effect::Print(history_lines(&records))], + Ok(records) => { + self.play_history = records.clone(); + vec![Effect::Print(history_lines(&records))] + } Err(message) => print_line(message), }, Some(Pending::Ping) => { @@ -386,6 +404,17 @@ impl ClientCore { let game = self.current_game_type().map(str::to_string); match command { + Command::Play { game, bet } => self.play_game(&game, bet), + Command::History { limit } => self.load_history(limit), + Command::ShowLobby => { + self.screen = Screen::Lobby; + Vec::new() + } + Command::Logout => { + self.logged_in = None; + self.screen = Screen::Lobby; + print_line("Logged out") + } Command::SetBet(bet) => { if game.as_deref() == Some("roulette") { return self.command(Command::SetChip(bet)); @@ -479,6 +508,7 @@ impl ClientCore { fn exit_game(&mut self) -> Vec { self.crash_started = None; + self.screen = Screen::Lobby; self.game.end_game(); vec![ Effect::ClearRegion, @@ -850,30 +880,18 @@ impl ClientCore { } } - fn cmd_play(&mut self, parts: &[&str]) -> Vec { + fn play_game(&mut self, game_type: &str, bet: Option) -> Vec { if !self.connected { return print_line("Not connected. Use 'connect :' first."); } if self.logged_in.is_none() { return print_line("Not logged in. Use 'login' first."); } - if parts.len() < 2 { - return self.available_games(Some("Usage: play [bet]")); - } - - let bet = match parts.get(2) { - Some(raw) => match parse_bet_arg(raw) { - Ok(bet) => Some(bet), - Err(message) => return print_line(message), - }, - None => None, - }; - if self.game.is_in_game() { return print_line("Already in a game. Use 'quit' to exit current game."); } - match self.game.enter_game(parts[1]) { + match self.game.enter_game(game_type) { Ok(()) => { if let Some(bet) = bet { let _ = self.game.set_bet(bet); @@ -891,6 +909,52 @@ impl ClientCore { } } + fn load_history(&mut self, limit: u32) -> Vec { + if !self.connected { + return print_line("Not connected. Use 'connect :' first."); + } + if self.logged_in.is_none() { + return print_line("Not logged in. Use 'login' first."); + } + if !(1..=MAX_HISTORY).contains(&limit) { + return print_line(format!("Usage: history [1-{MAX_HISTORY}]")); + } + + self.screen = Screen::History; + self.pending = Some(Pending::History); + let request = if limit == crate::history::DEFAULT_HISTORY { + "HISTORY".to_string() + } else { + format!("HISTORY {limit}") + }; + vec![Effect::SendLine(request)] + } + + fn cmd_play(&mut self, parts: &[&str]) -> Vec { + if !self.connected { + return print_line("Not connected. Use 'connect :' first."); + } + if self.logged_in.is_none() { + return print_line("Not logged in. Use 'login' first."); + } + if parts.len() < 2 { + return self.available_games(Some("Usage: play [bet]")); + } + + let bet = match parts.get(2) { + Some(raw) => match parse_bet_arg(raw) { + Ok(bet) => Some(bet), + Err(message) => return print_line(message), + }, + None => None, + }; + + self.command(Command::Play { + game: parts[1].to_string(), + bet, + }) + } + fn available_games(&self, usage: Option<&str>) -> Vec { let mut lines = vec!["Available games:".to_string()]; for (game_type, name) in self.game.get_available_games() { @@ -904,23 +968,15 @@ impl ClientCore { } fn cmd_history(&mut self, parts: &[&str]) -> Vec { - if !self.connected { - return print_line("Not connected. Use 'connect :' first."); - } - if self.logged_in.is_none() { - return print_line("Not logged in. Use 'login' first."); - } - - let request = match parts.get(1) { + let limit = match parts.get(1) { Some(raw) => match raw.parse::() { - Ok(count) if (1..=MAX_HISTORY).contains(&count) => format!("HISTORY {count}"), - _ => return print_line(format!("Usage: history [1-{MAX_HISTORY}]")), + Ok(count) => count, + Err(_) => return print_line(format!("Usage: history [1-{MAX_HISTORY}]")), }, - None => "HISTORY".to_string(), + None => crate::history::DEFAULT_HISTORY, }; - self.pending = Some(Pending::History); - vec![Effect::SendLine(request)] + self.command(Command::History { limit }) } fn cmd_info(&mut self) -> Vec { @@ -990,17 +1046,38 @@ impl ClientCore { /// Snapshot of the current screen for a frontend to draw. 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, + if let Some(state) = self.game.get_current_state() { + return 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, + }); + } + + Some(match self.screen { + Screen::History => View::History(HistoryView { + records: self.play_history.clone(), + }), + Screen::Lobby => View::Lobby(LobbyView { + connected: self.connected, + user: self.logged_in.clone(), + balance: self.balance, + games: self + .game + .get_available_games() + .into_iter() + .map(|(game, name)| (game.to_string(), name.to_string())) + .collect(), + }), }) } fn board_effects(&self) -> Vec { + if !self.is_in_game() { + return Vec::new(); + } self.view().map(Effect::Render).into_iter().collect() } diff --git a/src/client/tui.rs b/src/client/tui.rs index ddea5e2..9853edf 100644 --- a/src/client/tui.rs +++ b/src/client/tui.rs @@ -16,6 +16,8 @@ pub fn render_view(view: &View) -> Vec { View::Roulette(roulette) => RouletteGame::render_view(roulette), View::Blackjack(blackjack) => BlackjackGame::render_view(blackjack), View::Crash(crash) => CrashGame::render_view(crash), + // The terminal adapter keeps its command prompt for these screens. + View::Lobby(_) | View::History(_) => String::new(), }; rendered.lines().map(str::to_string).collect() } diff --git a/src/client/view.rs b/src/client/view.rs index 77efec6..903403c 100644 --- a/src/client/view.rs +++ b/src/client/view.rs @@ -241,9 +241,26 @@ impl CrashView { } } +/// The lobby: who is logged in, the balance, and what can be played. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LobbyView { + pub connected: bool, + pub user: Option, + pub balance: Option, + pub games: Vec<(String, String)>, +} + +/// The play log, already parsed. +#[derive(Debug, Clone, PartialEq)] +pub struct HistoryView { + pub records: Vec, +} + /// Everything a frontend can draw. #[derive(Debug, Clone, PartialEq)] pub enum View { + Lobby(LobbyView), + History(HistoryView), Slots(SlotsView), Roulette(RouletteView), Blackjack(BlackjackView), @@ -253,6 +270,10 @@ pub enum View { /// Semantic input. Frontends turn keys and clicks into these. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Command { + Play { game: String, bet: Option }, + History { limit: u32 }, + ShowLobby, + Logout, SetBet(u64), AdjustBet(i64), Leave, -- cgit v1.2.3