summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:36:26 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:36:26 +0100
commitc6c8ddb7e4117628549afdb382b0d46f830f242d (patch)
treecd59ac8bf690c942bce3b226f5777d01fde4896b
parentweb: restore the balance on a refresh into history (diff)
downloadzagan-bet-c6c8ddb7e4117628549afdb382b0d46f830f242d.tar.gz
zagan-bet-c6c8ddb7e4117628549afdb382b0d46f830f242d.zip
crash: show and verify the revealed seed in the TUI and GUI
The crash board gained a fairness line: the short commit while the round runs, then the short seed marked verified when sha256(seed) and the recomputed crash point both match. The terminal board grew from six to seven lines and the GUI sidebar shows the same values.
Diffstat (limited to '')
-rw-r--r--docs/TESTING.md2
-rw-r--r--gui/src/crash.rs34
-rw-r--r--src/client/games/crash.rs45
-rw-r--r--tests/client_core.rs33
4 files changed, 107 insertions, 7 deletions
diff --git a/docs/TESTING.md b/docs/TESTING.md
index fbd510b..141ea75 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -225,6 +225,8 @@ terminal client; a saved login from one signs in the other.
225 and `sha256(seed) == commit`; recompute the crash point from the seed. 225 and `sha256(seed) == commit`; recompute the crash point from the seed.
2268. Confirm `CRASH_BUSTED` is an `EVT`, not a `RES`, and that a second poll 2268. Confirm `CRASH_BUSTED` is an `EVT`, not a `RES`, and that a second poll
227 produces no repeated settlement. 227 produces no repeated settlement.
2289. The board's fairness line shows the short commit while running, then the
229 short seed and `verified` after the reveal (TUI and GUI).
228 230
229## Manual restart check 231## Manual restart check
230 232
diff --git a/gui/src/crash.rs b/gui/src/crash.rs
index 08113a8..a73c3bd 100644
--- a/gui/src/crash.rs
+++ b/gui/src/crash.rs
@@ -2,9 +2,18 @@
2 2
3use eframe::egui::{Align2, Color32, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2}; 3use eframe::egui::{Align2, Color32, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2};
4use zagan_bet::client::view::{Command, CrashView}; 4use zagan_bet::client::view::{Command, CrashView};
5use zagan_bet::crash::{crash_point_from_seed, seed_from_hex, verify_seed};
5 6
6use crate::{theme, widgets}; 7use crate::{theme, widgets};
7 8
9fn short_hash(hash: &str) -> String {
10 hash.char_indices()
11 .nth(12)
12 .map(|(index, _)| &hash[..index])
13 .unwrap_or(hash)
14 .to_string()
15}
16
8/// One sample of the running curve. 17/// One sample of the running curve.
9struct Sample { 18struct Sample {
10 seconds: f32, 19 seconds: f32,
@@ -136,6 +145,31 @@ fn sidebar(ui: &mut Ui, view: &CrashView, fx: &mut Fx) -> Option<Command> {
136 theme::GOLD, 145 theme::GOLD,
137 ); 146 );
138 147
148 if let Some(commit) = &view.commit {
149 widgets::row(ui, "Commit", &short_hash(commit), theme::TEXT_FAINT);
150 }
151 if let Some(seed) = &view.seed {
152 let commit_ok = view
153 .commit
154 .as_deref()
155 .map(|commit| verify_seed(seed, commit))
156 .unwrap_or(false);
157 let point_ok = seed_from_hex(seed)
158 .map(|bytes| {
159 view.crash_point
160 .map(|point| {
161 (crash_point_from_seed(&bytes) - point).abs() < 1e-9
162 })
163 .unwrap_or(false)
164 })
165 .unwrap_or(false);
166 if commit_ok && point_ok {
167 widgets::row(ui, "Seed ✓", &short_hash(seed), theme::WIN);
168 } else {
169 widgets::row(ui, "Seed ✗", &short_hash(seed), theme::LOSS);
170 }
171 }
172
139 ui.add_space(theme::SPACE_3); 173 ui.add_space(theme::SPACE_3);
140 status(ui, view); 174 status(ui, view);
141 175
diff --git a/src/client/games/crash.rs b/src/client/games/crash.rs
index affb642..c6dbbd5 100644
--- a/src/client/games/crash.rs
+++ b/src/client/games/crash.rs
@@ -1,7 +1,8 @@
1use super::traits::{Game, GameState}; 1use super::traits::{Game, GameState};
2use crate::client::view::CrashView; 2use crate::client::view::CrashView;
3use crate::crash::{crash_point_from_seed, seed_from_hex, verify_seed};
3 4
4pub const CRASH_BOARD_LINES: usize = 6; 5pub const CRASH_BOARD_LINES: usize = 7;
5 6
6pub struct CrashGame; 7pub struct CrashGame;
7 8
@@ -29,6 +30,38 @@ impl CrashGame {
29 } 30 }
30 } 31 }
31 32
33 /// Commitment line. The seed is shown only after the round ends, and the
34 /// client recomputes both the hash and the crash point.
35 fn fairness_text(view: &CrashView) -> String {
36 let short = |hash: &str| {
37 hash.char_indices()
38 .nth(12)
39 .map(|(index, _)| &hash[..index])
40 .unwrap_or(hash)
41 .to_string()
42 };
43 let Some(commit) = view.commit.as_deref() else {
44 return "Commit: —".to_string();
45 };
46 let Some(seed) = view.seed.as_deref() else {
47 return format!("Commit {} Seed hidden until the round ends", short(commit));
48 };
49 let commit_ok = verify_seed(seed, commit);
50 let point_ok = seed_from_hex(seed)
51 .map(|bytes| {
52 let expected = crash_point_from_seed(&bytes);
53 view.crash_point
54 .map(|point| (expected - point).abs() < 1e-9)
55 .unwrap_or(false)
56 })
57 .unwrap_or(false);
58 if commit_ok && point_ok {
59 format!("Commit {} Seed {} verified", short(commit), short(seed))
60 } else {
61 format!("Commit {} Seed {} VERIFICATION FAILED", short(commit), short(seed))
62 }
63 }
64
32 fn controls(view: &CrashView) -> &'static str { 65 fn controls(view: &CrashView) -> &'static str {
33 if view.status == "running" { 66 if view.status == "running" {
34 "[C] Cash out [Q] Forfeit" 67 "[C] Cash out [Q] Forfeit"
@@ -46,19 +79,21 @@ impl CrashGame {
46 79
47 if let Some(error) = &view.error { 80 if let Some(error) = &view.error {
48 return format!( 81 return format!(
49 "=== CRASH ===\r\nBalance: {balance} Kr Bet: {} Kr\r\n\r\nERROR: {error}\r\n\r\n[SPACE] Play [+/=/-] Bet [Q] Quit", 82 "=== CRASH ===\r\nBalance: {balance} Kr Bet: {} Kr\r\n\r\nERROR: {error}\r\n\r\n{}\r\n[SPACE] Play [+/=/-] Bet [Q] Quit",
50 view.bet 83 view.bet,
84 Self::fairness_text(view)
51 ); 85 );
52 } 86 }
53 87
54 format!( 88 format!(
55 "=== CRASH ===\r\nBalance: {} Kr Bet: {} Kr\r\nMultiplier: {:.2}x\r\nPotential: {} Kr\r\n{}\r\n{}", 89 "=== CRASH ===\r\nBalance: {} Kr Bet: {} Kr\r\nMultiplier: {:.2}x\r\nPotential: {} Kr\r\n{}\r\n{}\r\n{}",
56 balance, 90 balance,
57 view.bet, 91 view.bet,
58 view.multiplier, 92 view.multiplier,
59 view.potential, 93 view.potential,
60 Self::status_text(view), 94 Self::status_text(view),
61 Self::controls(view) 95 Self::controls(view),
96 Self::fairness_text(view)
62 ) 97 )
63 } 98 }
64} 99}
diff --git a/tests/client_core.rs b/tests/client_core.rs
index 3a2fb27..65fd1e6 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -987,9 +987,10 @@ fn crash_starts_a_round_and_draws_the_multiplier_locally() {
987 let mut h = Harness::new(); 987 let mut h = Harness::new();
988 h.enter_crash(); 988 h.enter_crash();
989 989
990 assert_eq!(h.board().len(), 6); 990 assert_eq!(h.board().len(), 7);
991 assert!(h.board()[4].contains("Press [SPACE] to start.")); 991 assert!(h.board()[4].contains("Press [SPACE] to start."));
992 assert!(h.board()[5].contains("[SPACE] Play")); 992 assert!(h.board()[5].contains("[SPACE] Play"));
993 assert!(h.board()[6].contains("Commit: —"));
993 994
994 h.key(Key::Char(' ')); 995 h.key(Key::Char(' '));
995 assert!(h.sent.iter().any(|line| line == "PLAY_START crash 10")); 996 assert!(h.sent.iter().any(|line| line == "PLAY_START crash 10"));
@@ -997,7 +998,7 @@ fn crash_starts_a_round_and_draws_the_multiplier_locally() {
997 let running = crash_state("running", 1.0, None, 0, 990); 998 let running = crash_state("running", 1.0, None, 0, 990);
998 h.reply(&crash_line("CRASH_START", &running)); 999 h.reply(&crash_line("CRASH_START", &running));
999 1000
1000 assert_eq!(h.board().len(), 6); 1001 assert_eq!(h.board().len(), 7);
1001 assert!(h.board()[2].contains("Multiplier: 1.00x")); 1002 assert!(h.board()[2].contains("Multiplier: 1.00x"));
1002 assert!(h.board()[3].contains("Potential: 10 Kr")); 1003 assert!(h.board()[3].contains("Potential: 10 Kr"));
1003 assert!(h.board()[4].contains("Rising")); 1004 assert!(h.board()[4].contains("Rising"));
@@ -1709,3 +1710,31 @@ fn a_crash_tick_updates_the_board_for_its_own_session() {
1709 h.push("crash.tick session=other t=1700000000200 m=9.0"); 1710 h.push("crash.tick session=other t=1700000000200 m=9.0");
1710 assert!(h.board()[2].contains("Multiplier: 1.50x")); 1711 assert!(h.board()[2].contains("Multiplier: 1.50x"));
1711} 1712}
1713
1714#[test]
1715fn the_crash_board_verifies_the_revealed_seed() {
1716 use zagan_bet::crash::{commit_seed, crash_point_from_seed};
1717
1718 let mut h = Harness::new();
1719 h.enter_crash();
1720 h.key(Key::Char(' '));
1721 let mut seed = [0u8; 32];
1722 seed[..8].copy_from_slice(&(0.67_f64 * 18_446_744_073_709_551_616.0).to_be_bytes());
1723 let commit = commit_seed(&seed);
1724 let point = crash_point_from_seed(&seed);
1725 let hex = seed
1726 .iter()
1727 .map(|byte| format!("{byte:02x}"))
1728 .collect::<String>();
1729
1730 let mut cashed = crash_state("cashed", 2.0, Some(point), 20, 1010);
1731 cashed.commit = Some(commit);
1732 cashed.seed = Some(hex);
1733 h.reply(&crash_line("CRASH_STATE", &cashed));
1734
1735 assert!(
1736 h.board()[6].contains("verified"),
1737 "fairness line: {}",
1738 h.board()[6]
1739 );
1740}