From 24e3bb54db579fb9aad8df2a591527dc77fe4cb4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 19:19:49 +0100 Subject: v4.0.7: finish the Phase 1 boundary with a command shell The terminal command line moves to client::shell: a typed line becomes a core Command or an adapter action (connect, login, register, set-credential, host maintenance). ClientCore loses submit_line and the last TUI-shaped effects (Interactive, SetPrompt, ListHosts, ForgetHost, AnimateRoulette); the adapter animates roulette from the Result view. Adds forget-login, which drops the stored credential for the pinned key and keeps trust, distinct from logout (session only) and forget-host (trust too). --- src/client/core.rs | 251 ++++++++++++----------------------------------------- 1 file changed, 57 insertions(+), 194 deletions(-) (limited to 'src/client/core.rs') diff --git a/src/client/core.rs b/src/client/core.rs index 280eb77..21c1cb2 100644 --- a/src/client/core.rs +++ b/src/client/core.rs @@ -6,20 +6,13 @@ use crate::client::games::response::{ use crate::client::view::{ BlackjackView, Command, CrashView, HistoryView, LobbyView, RouletteView, SlotsView, View, }; -use crate::client::ui::{help_lines, history_lines, parse_bet_arg}; +use crate::client::ui::{game_lines, history_lines}; use crate::crash::{CrashState, multiplier_at}; use crate::history::{PlayRecord, MAX_HISTORY}; use crate::roulette::{BetTarget, Bets}; use std::time::{Duration, Instant}; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum InteractiveCommand { - Login, - Register, - SetCred, - Connect(Option), -} /// High-level actions produced by the client core. The terminal adapter is /// responsible for turning these into crossterm calls and network writes. @@ -29,20 +22,12 @@ pub enum Effect { Print(Vec), /// Draw the current view in the board region. Render(View), - /// Run the roulette wheel animation, then draw the final view. - AnimateRoulette { winning: u8, view: View }, /// Remove the game board region. ClearRegion, /// Send one protocol line and feed the reply to `handle_reply`. SendLine(String), - /// Delegate an interactive prompt flow to the terminal adapter. - Interactive(InteractiveCommand), /// Drop the connection. Disconnect, - /// Let the adapter read the host database. - ListHosts, - /// Let the adapter delete a host from the database. - ForgetHost(String), /// Leave the program. Quit, } @@ -175,12 +160,8 @@ impl ClientCore { Some(Pending::RouletteSpin) => match parse_roulette_response(reply) { Ok(result) => { self.balance = Some(result.balance); - let winning = result.number; self.game.apply_roulette_result(result); - let Some(view) = self.view() else { - return Vec::new(); - }; - vec![Effect::AnimateRoulette { winning, view }] + self.board_effects() } Err(message) => { self.game.set_error(message); @@ -331,22 +312,41 @@ impl ClientCore { match command { Command::Login { user, credential } => { - if !self.connected { - return print_line("Not connected. Use 'connect :' first."); - } - self.login_error = None; - self.pending = Some(Pending::Login { user: user.clone() }); - vec![Effect::SendLine(format!("AUTH {user} {credential}"))] - } - Command::Register { user, credential } => { - if !self.connected { - return print_line("Not connected. Use 'connect :' first."); - } - self.login_error = None; - self.pending = Some(Pending::Register); - vec![Effect::SendLine(format!("REGISTER {user} {credential}"))] - } - Command::Play { game, bet } => self.play_game(&game, bet), + if !self.connected { + return print_line("Not connected. Use 'connect :' first."); + } + self.login_error = None; + self.pending = Some(Pending::Login { user: user.clone() }); + vec![Effect::SendLine(format!("AUTH {user} {credential}"))] + } + Command::Register { user, credential } => { + if !self.connected { + return print_line("Not connected. Use 'connect :' first."); + } + self.login_error = None; + self.pending = Some(Pending::Register); + vec![Effect::SendLine(format!("REGISTER {user} {credential}"))] + } + Command::Games => vec![Effect::Print(game_lines(&self.games()))], + Command::Ping => { + if !self.connected { + return print_line("Not connected. Use 'connect :' first."); + } + self.pending = Some(Pending::Ping); + vec![Effect::SendLine("PING".to_string())] + } + Command::Echo(message) => { + if !self.connected { + return print_line("Not connected. Use 'connect :' first."); + } + if message.trim().is_empty() { + return print_line("Usage: send "); + } + self.pending = Some(Pending::Echo); + vec![Effect::SendLine(message)] + } + Command::Info => self.info_effects(), + Command::Play { game, bet } => self.play_game(&game, bet), Command::History { limit } => self.load_history(limit), Command::ShowLobby => { self.screen = Screen::Lobby; @@ -672,106 +672,6 @@ impl ClientCore { vec![Effect::SendLine(format!("CRASH_CASHOUT {session}"))] } - /// Run one command line. The interactive frontend owns the buffer and the - /// history; the core only sees the finished line. - pub fn submit_line(&mut self, line: &str) -> Vec { - let line = line.trim().to_string(); - if line.is_empty() { - return Vec::new(); - } - - let parts: Vec<&str> = line.splitn(3, ' ').collect(); - match parts[0] { - "play" => self.cmd_play(&parts), - "games" => self.available_games(None), - "help" => vec![Effect::Print(help_lines())], - "connect" => { - if self.connected { - print_line("Already connected. Disconnect first.") - } else { - let target = parts.get(1).map(|value| value.to_string()); - vec![Effect::Interactive(InteractiveCommand::Connect(target))] - } - } - "login" => { - if !self.connected { - print_line("Not connected. Use 'connect :' first.") - } else if self.logged_in.is_some() { - print_line(format!( - "Already logged in as {}", - self.logged_in.as_deref().unwrap_or("?") - )) - } else { - vec![Effect::Interactive(InteractiveCommand::Login)] - } - } - "register" => { - if !self.connected { - print_line("Not connected. Use 'connect :' first.") - } else { - vec![Effect::Interactive(InteractiveCommand::Register)] - } - } - "setcred" => { - if self.host.is_none() { - print_line("No host connected. Use 'connect :' first.") - } else { - vec![Effect::Interactive(InteractiveCommand::SetCred)] - } - } - "logout" => { - self.logged_in = None; - print_line("Logged out") - } - "disconnect" => { - self.connected = false; - self.logged_in = None; - self.pending = None; - self.crash_started = None; - self.game.end_game(); - vec![ - Effect::ClearRegion, - Effect::Disconnect, - Effect::Print(vec!["Disconnected".to_string()]), - ] - } - "ping" => { - if !self.connected { - print_line("Not connected. Use 'connect :' first.") - } else { - self.pending = Some(Pending::Ping); - vec![Effect::SendLine("PING".to_string())] - } - } - "send" => { - if !self.connected { - print_line("Not connected. Use 'connect :' first.") - } else if parts.len() < 2 { - print_line("Usage: send ") - } else { - let message = parts[1..].join(" "); - self.pending = Some(Pending::Echo); - vec![Effect::SendLine(message)] - } - } - "info" => self.cmd_info(), - "history" => self.cmd_history(&parts), - "hosts" => vec![Effect::ListHosts], - "forget-host" => { - let target = parts - .get(1) - .map(|value| value.to_string()) - .or_else(|| self.host.clone()); - match target { - Some(target) => vec![Effect::ForgetHost(target)], - None => print_line("Usage: forget-host : (or connect first)"), - } - } - "quit" => vec![Effect::Quit], - _ => vec![Effect::Print(help_lines())], - } - } - fn play_game(&mut self, game_type: &str, bet: Option) -> Vec { if !self.connected { return print_line("Not connected. Use 'connect :' first."); @@ -822,56 +722,7 @@ impl ClientCore { 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() { - lines.push(format!(" - {} ({})", game_type, name)); - } - if let Some(usage) = usage { - lines.push(String::new()); - lines.push(usage.to_string()); - } - vec![Effect::Print(lines)] - } - - fn cmd_history(&mut self, parts: &[&str]) -> Vec { - let limit = match parts.get(1) { - Some(raw) => match raw.parse::() { - Ok(count) => count, - Err(_) => return print_line(format!("Usage: history [1-{MAX_HISTORY}]")), - }, - None => crate::history::DEFAULT_HISTORY, - }; - - self.command(Command::History { limit }) - } - - fn cmd_info(&mut self) -> Vec { + fn info_effects(&mut self) -> Vec { if !self.connected { return print_line("Not connected to any host."); } @@ -895,7 +746,24 @@ impl ClientCore { effects } - /// Snapshot of the current screen for a frontend to draw. + /// The games a frontend can offer. + pub fn games(&self) -> Vec<(String, String)> { + self.game + .get_available_games() + .into_iter() + .map(|(game, name)| (game.to_string(), name.to_string())) + .collect() + } + + /// Reset connection-dependent state after the adapter closed the socket. + /// + /// The transport belongs to the frontend, so it tells the core when the + /// link is gone. What to say about it is the frontend's business. + pub fn reset_after_disconnect(&mut self) { + self.set_connected(false); + self.game.end_game(); + } + pub fn view(&self) -> Option { if let Some(state) = self.game.get_current_state() { return Some(match state.game_type.as_str() { @@ -915,12 +783,7 @@ impl ClientCore { 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(), + games: self.games(), login_error: self.login_error.clone(), }), }) -- cgit v1.2.3