summaryrefslogtreecommitdiffstats
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/core.rs81
-rw-r--r--src/server/games/crash.rs76
2 files changed, 140 insertions, 17 deletions
diff --git a/src/server/core.rs b/src/server/core.rs
index 3cd6469..0b2efb4 100644
--- a/src/server/core.rs
+++ b/src/server/core.rs
@@ -1,22 +1,26 @@
1use bcrypt::{hash, verify, DEFAULT_COST}; 1use bcrypt::{DEFAULT_COST, hash, verify};
2use rusqlite::Connection; 2use rusqlite::Connection;
3use std::collections::HashMap; 3use std::collections::{HashMap, VecDeque};
4use std::sync::Arc; 4use std::sync::Arc;
5 5
6use crate::blackjack::BlackjackState; 6use crate::blackjack::BlackjackState;
7use crate::crash::CrashState; 7use crate::crash::CrashState;
8use crate::protocol::{REPLAY_CACHE_LEN, Request};
8use crate::server::database; 9use crate::server::database;
9use crate::version::{download_url, hello_line, Version, VERSION};
10use crate::server::games::blackjack::{BlackjackGame, BlackjackHand}; 10use crate::server::games::blackjack::{BlackjackGame, BlackjackHand};
11use crate::server::games::crash::{Clock, CrashGame, CrashRound, RealClock}; 11use crate::server::games::crash::{Clock, CrashGame, CrashRound, RealClock};
12use crate::server::games::registry::GameRegistry; 12use crate::server::games::registry::GameRegistry;
13use crate::server::games::session::SessionManager; 13use crate::server::games::session::SessionManager;
14use crate::server::games::traits::{GameError, GamePhase, GameState}; 14use crate::server::games::traits::{GameError, GamePhase, GameState};
15use crate::version::{VERSION, Version, download_url, hello_line, pong_line, unix_time_ms};
15 16
16#[derive(Debug, Default)] 17#[derive(Debug, Default)]
17pub struct ConnectionState { 18pub struct ConnectionState {
18 user: Option<String>, 19 user: Option<String>,
19 greeted: bool, 20 greeted: bool,
21 /// Last replies on this connection, oldest first. Request ids are unique
22 /// while cached, so a retried request replays its first result.
23 replays: VecDeque<(u64, String, String)>,
20} 24}
21 25
22/// Settle reservations left behind by a previous process. 26/// Settle reservations left behind by a previous process.
@@ -39,6 +43,27 @@ impl ConnectionState {
39 pub fn greeted(&self) -> bool { 43 pub fn greeted(&self) -> bool {
40 self.greeted 44 self.greeted
41 } 45 }
46
47 fn replay(&self, id: u64, payload: &str) -> Option<Result<String, String>> {
48 self.replays
49 .iter()
50 .find(|(cached_id, _, _)| *cached_id == id)
51 .map(|(_, cached_payload, reply)| {
52 if cached_payload == payload {
53 Ok(reply.clone())
54 } else {
55 Err("ERROR Request id reused with a different command".to_string())
56 }
57 })
58 }
59
60 fn remember(&mut self, id: u64, payload: &str, reply: &str) {
61 if self.replays.len() >= REPLAY_CACHE_LEN {
62 self.replays.pop_front();
63 }
64 self.replays
65 .push_back((id, payload.to_string(), reply.to_string()));
66 }
42} 67}
43 68
44pub struct ServerCore { 69pub struct ServerCore {
@@ -49,6 +74,8 @@ pub struct ServerCore {
49 blackjack: BlackjackGame, 74 blackjack: BlackjackGame,
50 crash: CrashGame, 75 crash: CrashGame,
51 clock: Arc<dyn Clock>, 76 clock: Arc<dyn Clock>,
77 /// Last time a crash tick was pushed, in seconds on the game clock.
78 last_tick: HashMap<String, f64>,
52} 79}
53 80
54impl ServerCore { 81impl ServerCore {
@@ -65,6 +92,7 @@ impl ServerCore {
65 blackjack: BlackjackGame::new(), 92 blackjack: BlackjackGame::new(),
66 crash: CrashGame::new(), 93 crash: CrashGame::new(),
67 clock: Arc::new(RealClock::new()), 94 clock: Arc::new(RealClock::new()),
95 last_tick: HashMap::new(),
68 } 96 }
69 } 97 }
70 98
@@ -96,6 +124,21 @@ impl ServerCore {
96 Ok(Self::new(conn)) 124 Ok(Self::new(conn))
97 } 125 }
98 126
127 /// Handle one enveloped request, replaying a cached reply for a repeated
128 /// request id. The payload grammar is unchanged; the envelope only adds
129 /// correlation and idempotency.
130 pub fn handle_request(&mut self, state: &mut ConnectionState, request: &Request) -> String {
131 if let Some(reply) = state.replay(request.id, &request.payload) {
132 return match reply {
133 Ok(reply) => reply,
134 Err(message) => message,
135 };
136 }
137 let reply = self.handle_message(state, &request.payload);
138 state.remember(request.id, &request.payload, &reply);
139 reply
140 }
141
99 pub fn handle_message(&mut self, state: &mut ConnectionState, msg: &str) -> String { 142 pub fn handle_message(&mut self, state: &mut ConnectionState, msg: &str) -> String {
100 let parts: Vec<&str> = msg.splitn(5, ' ').collect(); 143 let parts: Vec<&str> = msg.splitn(5, ' ').collect();
101 144
@@ -201,7 +244,7 @@ impl ServerCore {
201 } 244 }
202 } 245 }
203 } 246 }
204 "PING" => "PONG".to_string(), 247 "PING" => pong_line(unix_time_ms()),
205 "INFO" => self.handle_info(state), 248 "INFO" => self.handle_info(state),
206 _ => msg.to_string(), 249 _ => msg.to_string(),
207 } 250 }
@@ -212,7 +255,8 @@ impl ServerCore {
212 return "ERROR Invalid HELLO format. Usage: HELLO <version>".to_string(); 255 return "ERROR Invalid HELLO format. Usage: HELLO <version>".to_string();
213 } 256 }
214 257
215 let (Some(client), Some(server)) = (Version::parse(parts[1]), Version::parse(VERSION)) else { 258 let (Some(client), Some(server)) = (Version::parse(parts[1]), Version::parse(VERSION))
259 else {
216 return "ERROR Invalid version".to_string(); 260 return "ERROR Invalid version".to_string();
217 }; 261 };
218 262
@@ -225,7 +269,7 @@ impl ServerCore {
225 } 269 }
226 270
227 state.greeted = true; 271 state.greeted = true;
228 hello_line(VERSION, VERSION, &download_url()) 272 hello_line(VERSION, VERSION, &download_url(), unix_time_ms())
229 } 273 }
230 274
231 fn handle_register(&mut self, username: &str, credential: &str) -> String { 275 fn handle_register(&mut self, username: &str, credential: &str) -> String {
@@ -529,8 +573,8 @@ impl ServerCore {
529 573
530 // The session is the audit guard for the deduction, so it exists before 574 // The session is the audit guard for the deduction, so it exists before
531 // the balance is touched. 575 // the balance is touched.
532 let crash_point = self.crash.next_crash_point(); 576 let seed = self.crash.next_seed();
533 let round = CrashRound::new(bet as u64, crash_point, self.clock.now()); 577 let round = CrashRound::new(bet as u64, seed, self.clock.now(), unix_time_ms());
534 let mut data = HashMap::new(); 578 let mut data = HashMap::new();
535 data.insert("round".to_string(), round.to_json()); 579 data.insert("round".to_string(), round.to_json());
536 data.insert("balance".to_string(), balance.to_string()); 580 data.insert("balance".to_string(), balance.to_string());
@@ -643,11 +687,27 @@ impl ServerCore {
643 if !round.is_running() { 687 if !round.is_running() {
644 continue; 688 continue;
645 } 689 }
646 let elapsed = (self.clock.now() - round.started_at).max(0.0); 690 let now = self.clock.now();
647 if crate::crash::multiplier_at_secs(elapsed) < round.crash_point { 691 let elapsed = (now - round.started_at).max(0.0);
692 let multiplier = crate::crash::multiplier_at_secs(elapsed);
693 if multiplier < round.crash_point {
694 let last = self
695 .last_tick
696 .get(&session.session_id)
697 .copied()
698 .unwrap_or(f64::NEG_INFINITY);
699 if now - last >= 0.1 {
700 self.last_tick.insert(session.session_id.clone(), now);
701 out.push(format!(
702 "crash.tick session={} t={} m={multiplier:.4}",
703 session.session_id,
704 unix_time_ms()
705 ));
706 }
648 continue; 707 continue;
649 } 708 }
650 round.crash(); 709 round.crash();
710 self.last_tick.remove(&session.session_id);
651 match self.settle_crash(user, &session.session_id, &round, 0) { 711 match self.settle_crash(user, &session.session_id, &round, 0) {
652 Ok(balance) => { 712 Ok(balance) => {
653 let state = round.state(&session.session_id, balance); 713 let state = round.state(&session.session_id, balance);
@@ -678,6 +738,7 @@ impl ServerCore {
678 payout as i64, 738 payout as i64,
679 &details, 739 &details,
680 ); 740 );
741 self.last_tick.remove(session_id);
681 let _ = self.sessions.remove_session(session_id); 742 let _ = self.sessions.remove_session(session_id);
682 result.map_err(|error| error.to_string()) 743 result.map_err(|error| error.to_string())
683 } 744 }
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