diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-08 03:36:26 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-08 03:36:26 +0100 |
| commit | c6c8ddb7e4117628549afdb382b0d46f830f242d (patch) | |
| tree | cd59ac8bf690c942bce3b226f5777d01fde4896b | |
| parent | web: restore the balance on a refresh into history (diff) | |
| download | zagan-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.md | 2 | ||||
| -rw-r--r-- | gui/src/crash.rs | 34 | ||||
| -rw-r--r-- | src/client/games/crash.rs | 45 | ||||
| -rw-r--r-- | tests/client_core.rs | 33 |
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. |
| 226 | 8. Confirm `CRASH_BUSTED` is an `EVT`, not a `RES`, and that a second poll | 226 | 8. Confirm `CRASH_BUSTED` is an `EVT`, not a `RES`, and that a second poll |
| 227 | produces no repeated settlement. | 227 | produces no repeated settlement. |
| 228 | 9. 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 | ||
| 3 | use eframe::egui::{Align2, Color32, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2}; | 3 | use eframe::egui::{Align2, Color32, Pos2, Rect, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2}; |
| 4 | use zagan_bet::client::view::{Command, CrashView}; | 4 | use zagan_bet::client::view::{Command, CrashView}; |
| 5 | use zagan_bet::crash::{crash_point_from_seed, seed_from_hex, verify_seed}; | ||
| 5 | 6 | ||
| 6 | use crate::{theme, widgets}; | 7 | use crate::{theme, widgets}; |
| 7 | 8 | ||
| 9 | fn 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. |
| 9 | struct Sample { | 18 | struct 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 @@ | |||
| 1 | use super::traits::{Game, GameState}; | 1 | use super::traits::{Game, GameState}; |
| 2 | use crate::client::view::CrashView; | 2 | use crate::client::view::CrashView; |
| 3 | use crate::crash::{crash_point_from_seed, seed_from_hex, verify_seed}; | ||
| 3 | 4 | ||
| 4 | pub const CRASH_BOARD_LINES: usize = 6; | 5 | pub const CRASH_BOARD_LINES: usize = 7; |
| 5 | 6 | ||
| 6 | pub struct CrashGame; | 7 | pub 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] | ||
| 1715 | fn 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 | } | ||
