use super::traits::{Game, GameState}; use crate::client::view::CrashView; use crate::crash::{crash_point_from_seed, seed_from_hex, verify_seed}; pub const CRASH_BOARD_LINES: usize = 7; pub struct CrashGame; impl CrashGame { pub fn new() -> Self { Self } fn status_text(view: &CrashView) -> String { match view.status.as_str() { "starting" => "Starting...".to_string(), "running" => "Rising... cash out before it crashes.".to_string(), "cashed" => format!( "Cashed out at {:.2}x Won {} Kr", view.multiplier, view.payout ), "crashed" => format!( "CRASHED at {}x Bet lost", view.crash_point .map(|point| format!("{point:.2}")) .unwrap_or_else(|| "?".to_string()) ), "forfeit" => "Forfeited. Bet lost.".to_string(), _ => "Press [SPACE] to start.".to_string(), } } /// Commitment line. The seed is shown only after the round ends, and the /// client recomputes both the hash and the crash point. fn fairness_text(view: &CrashView) -> String { let short = |hash: &str| { hash.char_indices() .nth(12) .map(|(index, _)| &hash[..index]) .unwrap_or(hash) .to_string() }; let Some(commit) = view.commit.as_deref() else { return "Commit: —".to_string(); }; let Some(seed) = view.seed.as_deref() else { return format!("Commit {} Seed hidden until the round ends", short(commit)); }; let commit_ok = verify_seed(seed, commit); let point_ok = seed_from_hex(seed) .map(|bytes| { let expected = crash_point_from_seed(&bytes); view.crash_point .map(|point| (expected - point).abs() < 1e-9) .unwrap_or(false) }) .unwrap_or(false); if commit_ok && point_ok { format!("Commit {} Seed {} verified", short(commit), short(seed)) } else { format!("Commit {} Seed {} VERIFICATION FAILED", short(commit), short(seed)) } } fn controls(view: &CrashView) -> &'static str { if view.status == "running" { "[C] Cash out [Q] Forfeit" } else { "[SPACE] Play [+/=/-] Bet [Q] Quit" } } /// Render a crash view to terminal lines. The GUI paints the same view. pub fn render_view(view: &CrashView) -> String { let balance = view .balance .map(|balance| balance.to_string()) .unwrap_or_else(|| "?".to_string()); if let Some(error) = &view.error { return format!( "=== CRASH ===\r\nBalance: {balance} Kr Bet: {} Kr\r\n\r\nERROR: {error}\r\n\r\n{}\r\n[SPACE] Play [+/=/-] Bet [Q] Quit", view.bet, Self::fairness_text(view) ); } format!( "=== CRASH ===\r\nBalance: {} Kr Bet: {} Kr\r\nMultiplier: {:.2}x\r\nPotential: {} Kr\r\n{}\r\n{}\r\n{}", balance, view.bet, view.multiplier, view.potential, Self::status_text(view), Self::controls(view), Self::fairness_text(view) ) } } impl Game for CrashGame { fn get_name(&self) -> &'static str { "Crash" } fn render(&self, state: &GameState) -> String { Self::render_view(&CrashView::from_state(state)) } fn render_lines(&self, _state: &GameState) -> usize { CRASH_BOARD_LINES } fn get_help(&self) -> String { "CRASH\n\ =====\n\ The multiplier rises from 1.00x and crashes at a hidden point.\n\ Cash out before it crashes to win bet x multiplier.\n\ A round can crash at 1.00x, so an instant cashout still risks the bet.\n\ \n\ C - cash out at the server's current multiplier\n\ SPACE - start the next round\n\ +/- - change the next round's bet\n\ Q - forfeit an active round (bet is lost), or leave when idle" .to_string() } }