From 058432130628d00764663ecf819a9adc6793768d Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 8 Nov 2026 03:29:35 +0100 Subject: v5.0.0: protocol v5 and the browser frontend Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO. --- tests/web_types.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/web_types.rs (limited to 'tests/web_types.rs') diff --git a/tests/web_types.rs b/tests/web_types.rs new file mode 100644 index 0000000..21af474 --- /dev/null +++ b/tests/web_types.rs @@ -0,0 +1,138 @@ +//! The browser and gateway exchange serde JSON for `Command`/`View`. These +//! tests pin the wire shape the TypeScript app renders. + +use zagan_bet::client::games::roulette::{Cursor, Direction}; +use zagan_bet::client::view::{ + BlackjackView, Command, CrashView, HistoryView, LobbyView, RoulettePhase, RouletteView, + SlotsPhase, SlotsView, View, +}; +use zagan_bet::roulette::{BetTarget, Bets, Outside}; + +#[test] +fn browser_commands_deserialize() { + let login: Command = + serde_json::from_str(r#"{"Login":{"user":"alice","credential":"abc"}}"#).unwrap(); + assert_eq!( + login, + Command::Login { + user: "alice".to_string(), + credential: "abc".to_string() + } + ); + + let play: Command = serde_json::from_str(r#"{"Play":{"game":"crash","bet":25}}"#).unwrap(); + assert_eq!( + play, + Command::Play { + game: "crash".to_string(), + bet: Some(25) + } + ); + + let bet: Command = + serde_json::from_str(r#"{"PlaceBetAt":{"bet":"outside","kind":"red"}}"#).unwrap(); + assert_eq!( + bet, + Command::PlaceBetAt(BetTarget::Outside { kind: Outside::Red }) + ); + + let split: Command = + serde_json::from_str(r#"{"PlaceBetAt":{"bet":"split","a":1,"b":2}}"#).unwrap(); + assert_eq!(split, Command::PlaceBetAt(BetTarget::Split { a: 1, b: 2 })); + + assert_eq!( + serde_json::from_str::(r#""Spin""#).unwrap(), + Command::Spin + ); + assert_eq!( + serde_json::from_str::(r#""ShowLobby""#).unwrap(), + Command::ShowLobby + ); + assert_eq!( + serde_json::from_str::(r#"{"SetBet":50}"#).unwrap(), + Command::SetBet(50) + ); + assert_eq!( + serde_json::from_str::(r#"{"MoveCursor":"Left"}"#).unwrap(), + Command::MoveCursor(Direction::Left) + ); +} + +#[test] +fn views_serialize_with_a_screen_tag() { + let lobby = View::Lobby(LobbyView { + connected: true, + user: Some("alice".to_string()), + account: Some("1".to_string()), + balance: Some(1000), + games: vec![("slots".to_string(), "Slots".to_string())], + login_error: None, + }); + let json = serde_json::to_value(&lobby).unwrap(); + assert_eq!(json["screen"], "lobby"); + assert_eq!(json["user"], "alice"); + + let slots = View::Slots(SlotsView { + bet: 10, + balance: Some(990), + reels: "CLOGD".chars().collect(), + win_cells: vec![0], + win_lines: vec![1], + win_type: "win".to_string(), + winnings: Some(20), + phase: SlotsPhase::Result, + error: None, + }); + let json = serde_json::to_value(&slots).unwrap(); + assert_eq!(json["screen"], "slots"); + assert_eq!(json["phase"], "result"); + assert_eq!(json["reels"][0], "C"); + + let roulette = View::Roulette(RouletteView { + bet_amount: 10, + balance: Some(1000), + chip: 25, + bets: Bets::default(), + cursor: Cursor::default(), + phase: RoulettePhase::Placing, + number: None, + color: None, + winnings: None, + error: None, + }); + let json = serde_json::to_value(&roulette).unwrap(); + assert_eq!(json["screen"], "roulette"); + assert_eq!(json["phase"], "placing"); + + let blackjack = View::Blackjack(BlackjackView { + status: "ready".to_string(), + balance: Some(1000), + bet: 10, + hand: None, + error: None, + }); + assert_eq!( + serde_json::to_value(&blackjack).unwrap()["screen"], + "blackjack" + ); + + let crash = View::Crash(CrashView { + status: "running".to_string(), + balance: Some(990), + bet: 10, + multiplier: 1.5, + potential: 15, + crash_point: None, + payout: 0, + started_at: 1_700_000_000_000, + commit: Some("ab".repeat(32)), + seed: None, + error: None, + }); + let json = serde_json::to_value(&crash).unwrap(); + assert_eq!(json["screen"], "crash"); + assert_eq!(json["commit"], "ab".repeat(32)); + + let history = View::History(HistoryView { records: vec![] }); + assert_eq!(serde_json::to_value(&history).unwrap()["screen"], "history"); +} -- cgit v1.2.3