summaryrefslogtreecommitdiffstats
path: root/src/client/games/crash.rs
blob: c6dbbd5a16e26451757099cec529f1df1b9e70eb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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()
    }
}