summaryrefslogtreecommitdiffstats
path: root/src/server/games
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
commit058432130628d00764663ecf819a9adc6793768d (patch)
treed3f49f111fbfaa48647cca2600adcf9017af2d30 /src/server/games
parentweb: serve latest.php through PHP and refuse stray PHP files (diff)
downloadzagan-bet-058432130628d00764663ecf819a9adc6793768d.tar.gz
zagan-bet-058432130628d00764663ecf819a9adc6793768d.zip
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.
Diffstat (limited to 'src/server/games')
-rw-r--r--src/server/games/crash.rs76
1 files changed, 69 insertions, 7 deletions
diff --git a/src/server/games/crash.rs b/src/server/games/crash.rs
index ba73c7f..1728afc 100644
--- a/src/server/games/crash.rs
+++ b/src/server/games/crash.rs
@@ -1,5 +1,8 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; 1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use crate::crash::{crash_point_from_uniform, payout, CrashState}; 2use crate::crash::{
3 CrashState, SEED_LEN, commit_seed, crash_point_from_seed, payout, seed_to_uniform,
4};
5use rand::RngCore;
3use serde::{Deserialize, Serialize}; 6use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, VecDeque}; 7use std::collections::{HashMap, VecDeque};
5use std::sync::{Arc, Mutex}; 8use std::sync::{Arc, Mutex};
@@ -64,7 +67,14 @@ pub struct CrashRound {
64 pub bet: u64, 67 pub bet: u64,
65 /// Drawn at start, revealed only when the round ends. 68 /// Drawn at start, revealed only when the round ends.
66 pub crash_point: f64, 69 pub crash_point: f64,
70 /// The 32 random bytes the crash point commits to, hex encoded. Kept on
71 /// the server until the round ends.
72 pub seed: String,
73 /// `sha256(seed)` in hex, sent with the start reply.
74 pub commit: String,
67 pub started_at: f64, 75 pub started_at: f64,
76 /// Server Unix time at start, milliseconds, for clock sync.
77 pub started_at_ms: u64,
68 /// `running`, `cashed`, `crashed` or `forfeit`. 78 /// `running`, `cashed`, `crashed` or `forfeit`.
69 pub phase: String, 79 pub phase: String,
70 pub result: String, 80 pub result: String,
@@ -74,11 +84,15 @@ pub struct CrashRound {
74} 84}
75 85
76impl CrashRound { 86impl CrashRound {
77 pub fn new(bet: u64, crash_point: f64, started_at: f64) -> Self { 87 pub fn new(bet: u64, seed: [u8; SEED_LEN], started_at: f64, started_at_ms: u64) -> Self {
88 let crash_point = crash_point_from_seed(&seed);
78 Self { 89 Self {
79 bet, 90 bet,
80 crash_point, 91 crash_point,
92 seed: seed_hex(&seed),
93 commit: commit_seed(&seed),
81 started_at, 94 started_at,
95 started_at_ms,
82 phase: "running".to_string(), 96 phase: "running".to_string(),
83 result: "playing".to_string(), 97 result: "playing".to_string(),
84 multiplier: 1.0, 98 multiplier: 1.0,
@@ -120,6 +134,7 @@ impl CrashRound {
120 balance, 134 balance,
121 multiplier: self.multiplier, 135 multiplier: self.multiplier,
122 potential: payout(self.bet, self.multiplier), 136 potential: payout(self.bet, self.multiplier),
137 started_at: self.started_at_ms,
123 crash_point: if self.is_running() { 138 crash_point: if self.is_running() {
124 None 139 None
125 } else { 140 } else {
@@ -127,6 +142,12 @@ impl CrashRound {
127 }, 142 },
128 payout: self.payout, 143 payout: self.payout,
129 result: self.result.clone(), 144 result: self.result.clone(),
145 commit: Some(self.commit.clone()),
146 seed: if self.is_running() {
147 None
148 } else {
149 Some(self.seed.clone())
150 },
130 } 151 }
131 } 152 }
132 153
@@ -146,6 +167,34 @@ pub struct CrashGame {
146 uniforms: VecDeque<f64>, 167 uniforms: VecDeque<f64>,
147} 168}
148 169
170/// Hex encode a round seed. Lowercase, no separators.
171fn seed_hex(seed: &[u8]) -> String {
172 let mut out = String::with_capacity(seed.len() * 2);
173 for byte in seed {
174 use std::fmt::Write as _;
175 let _ = write!(out, "{byte:02x}");
176 }
177 out
178}
179
180/// Draw a seed whose committed point matches a queued test uniform.
181///
182/// The first eight bytes are the big-endian integer closest to
183/// `uniform * 2^64`; the remaining bytes are zero. This keeps the existing
184/// deterministic crash-point tests meaningful while every round still has a
185/// real seed.
186fn seed_from_uniform(uniform: f64) -> [u8; SEED_LEN] {
187 let uniform = if uniform.is_finite() {
188 uniform.clamp(0.0, 0.999_999_999_999_999_9)
189 } else {
190 0.0
191 };
192 let value = (uniform * 18_446_744_073_709_551_616.0) as u64;
193 let mut seed = [0u8; SEED_LEN];
194 seed[..8].copy_from_slice(&value.to_be_bytes());
195 seed
196}
197
149impl CrashGame { 198impl CrashGame {
150 pub fn new() -> Self { 199 pub fn new() -> Self {
151 Self { 200 Self {
@@ -163,12 +212,25 @@ impl CrashGame {
163 } 212 }
164 } 213 }
165 214
215 /// Draw the next round seed. Tests supply uniforms; production uses the
216 /// operating system's random source.
217 pub fn next_seed(&mut self) -> [u8; SEED_LEN] {
218 if let Some(uniform) = self.uniforms.pop_front() {
219 return seed_from_uniform(uniform);
220 }
221 let mut seed = [0u8; SEED_LEN];
222 rand::thread_rng().fill_bytes(&mut seed);
223 seed
224 }
225
226 /// Kept for callers that only need the point a queued uniform encodes.
166 pub fn next_crash_point(&mut self) -> f64 { 227 pub fn next_crash_point(&mut self) -> f64 {
167 let uniform = self 228 crash_point_from_seed(&self.next_seed())
168 .uniforms 229 }
169 .pop_front() 230
170 .unwrap_or_else(rand::random::<f64>); 231 /// The uniform encoded by a seed, exposed for verification tests.
171 crash_point_from_uniform(uniform) 232 pub fn uniform_of(seed: &[u8; SEED_LEN]) -> f64 {
233 seed_to_uniform(seed)
172 } 234 }
173} 235}
174 236