//! What a frontend draws and what it can ask the core to do. //! //! The core owns state and navigation; adapters map input to `Command` and //! render `View`. Terminal text lives in [`crate::client::tui`], not here. use crate::blackjack::BlackjackState; use crate::client::games::roulette::{Cursor, Direction}; use crate::client::games::traits::{GamePhase, GameState}; use crate::roulette::{BetTarget, Bets}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RoulettePhase { Placing, Spinning, Result, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RouletteView { pub bet_amount: u64, pub balance: Option, pub chip: u64, pub bets: Bets, pub cursor: Cursor, pub phase: RoulettePhase, pub number: Option, pub color: Option, pub winnings: Option, pub error: Option, } impl RouletteView { pub fn from_state(state: &GameState) -> Self { let bets = state .data .get("bets") .and_then(|raw| Bets::from_json(raw).ok()) .unwrap_or_default(); let chip = state .data .get("chip") .and_then(|value| value.parse().ok()) .unwrap_or(10); let cursor = state .data .get("cursor") .map(|raw| Cursor::parse(raw)) .unwrap_or_default(); let phase = match state.data.get("status").map(String::as_str) { Some("spinning") => RoulettePhase::Spinning, Some("result") => RoulettePhase::Result, _ => RoulettePhase::Placing, }; let number = state.data.get("number").and_then(|value| value.parse().ok()); let color = state.data.get("color").cloned(); let winnings = state .data .get("winnings") .and_then(|value| value.parse().ok()); let balance = state .data .get("balance") .and_then(|value| value.parse().ok()); let error = state .data .get("error") .cloned() .filter(|message| !message.is_empty()); Self { bet_amount: state.bet_amount, balance, chip, bets, cursor, phase, number, color, winnings, error, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SlotsPhase { Ready, Spinning, Result, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SlotsView { pub bet: u64, pub balance: Option, pub reels: Vec, pub win_cells: Vec, pub win_lines: Vec, pub win_type: String, pub winnings: Option, pub phase: SlotsPhase, pub error: Option, } fn parse_indices(raw: Option<&String>) -> Vec { raw.map(|value| { value .split(',') .filter_map(|part| part.trim().parse().ok()) .collect() }) .unwrap_or_default() } impl SlotsView { pub fn from_state(state: &GameState) -> Self { let reels = state .data .get("reels") .map(|raw| raw.chars().collect()) .unwrap_or_default(); let phase = match state.phase { GamePhase::Playing => SlotsPhase::Spinning, GamePhase::Finished => SlotsPhase::Result, GamePhase::WaitingToStart => SlotsPhase::Ready, }; Self { bet: state.bet_amount, balance: state .data .get("balance") .and_then(|value| value.parse().ok()), reels, win_cells: parse_indices(state.data.get("win_cells")), win_lines: parse_indices(state.data.get("win_lines")), win_type: state.data.get("win_type").cloned().unwrap_or_default(), winnings: state .data .get("winnings") .and_then(|value| value.parse().ok()), phase, error: state .data .get("error") .cloned() .filter(|message| !message.is_empty()), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct BlackjackView { pub status: String, pub balance: Option, pub bet: u64, pub hand: Option, pub error: Option, } impl BlackjackView { pub fn from_state(state: &GameState) -> Self { let hand = state .data .get("blackjack") .and_then(|raw| serde_json::from_str(raw).ok()); Self { status: state .data .get("status") .cloned() .unwrap_or_else(|| "ready".to_string()), balance: state .data .get("balance") .and_then(|value| value.parse().ok()), bet: state.bet_amount, hand, error: state .data .get("error") .cloned() .filter(|message| !message.is_empty()), } } } #[derive(Debug, Clone, PartialEq)] pub struct CrashView { pub status: String, pub balance: Option, pub bet: u64, pub multiplier: f64, pub potential: u64, pub crash_point: Option, pub payout: u64, pub error: Option, } impl CrashView { pub fn from_state(state: &GameState) -> Self { let bet = state.bet_amount; Self { status: state .data .get("status") .cloned() .unwrap_or_else(|| "ready".to_string()), balance: state .data .get("balance") .and_then(|value| value.parse().ok()), bet, multiplier: state .data .get("multiplier") .and_then(|value| value.parse::().ok()) .unwrap_or(1.0), potential: state .data .get("potential") .and_then(|value| value.parse::().ok()) .unwrap_or(bet), crash_point: state .data .get("crash_point") .and_then(|value| value.parse::().ok()), payout: state .data .get("payout") .and_then(|value| value.parse::().ok()) .unwrap_or(0), error: state .data .get("error") .cloned() .filter(|message| !message.is_empty()), } } } /// 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), Crash(CrashView), } /// 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, Spin, Deal, Hit, Stand, Double, Start, CashOut, Forfeit, MoveCursor(Direction), PlaceBet, PlaceBetAt(BetTarget), ClearBets, SetChip(u64), AdjustChip(i64), }