diff options
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 | } | ||
