summaryrefslogtreecommitdiffstats
path: root/src/server/games
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-29 20:43:09 +0100
committersalmonik <salmonik@tuta.io>2026-10-29 20:43:09 +0100
commit6e2787d792de64c003eff03b62a2df798ef851c0 (patch)
tree0e9bf97b2f71df5fb9eb00d5195d32b8326886ff /src/server/games
parentv2.2.1: forfeit blackjack bets when leaving or disconnecting (diff)
downloadzagan-bet-6e2787d792de64c003eff03b62a2df798ef851c0.tar.gz
zagan-bet-6e2787d792de64c003eff03b62a2df798ef851c0.zip
v2.3.0: add crash with local multiplier and pushed crash event
Diffstat (limited to '')
-rw-r--r--src/server/games/crash.rs216
-rw-r--r--src/server/games/mod.rs1
-rw-r--r--src/server/games/registry.rs2
3 files changed, 219 insertions, 0 deletions
diff --git a/src/server/games/crash.rs b/src/server/games/crash.rs
new file mode 100644
index 0000000..ba73c7f
--- /dev/null
+++ b/src/server/games/crash.rs
@@ -0,0 +1,216 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use crate::crash::{crash_point_from_uniform, payout, CrashState};
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, VecDeque};
5use std::sync::{Arc, Mutex};
6use std::time::Instant;
7
8/// Monotonic seconds. Injected so tests can drive a round without sleeping.
9pub trait Clock: Send + Sync {
10 fn now(&self) -> f64;
11}
12
13pub struct RealClock {
14 origin: Instant,
15}
16
17impl RealClock {
18 pub fn new() -> Self {
19 Self {
20 origin: Instant::now(),
21 }
22 }
23}
24
25impl Clock for RealClock {
26 fn now(&self) -> f64 {
27 self.origin.elapsed().as_secs_f64()
28 }
29}
30
31/// A clock a test can move by hand. Clones share the same instant.
32#[derive(Clone, Default)]
33pub struct ManualClock {
34 seconds: Arc<Mutex<f64>>,
35}
36
37impl ManualClock {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn set(&self, seconds: f64) {
43 *self.seconds.lock().unwrap() = seconds;
44 }
45
46 pub fn advance(&self, seconds: f64) {
47 *self.seconds.lock().unwrap() += seconds;
48 }
49
50 pub fn seconds(&self) -> f64 {
51 *self.seconds.lock().unwrap()
52 }
53}
54
55impl Clock for ManualClock {
56 fn now(&self) -> f64 {
57 self.seconds()
58 }
59}
60
61/// One crash round. Lives in the game session's `data` while it runs.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CrashRound {
64 pub bet: u64,
65 /// Drawn at start, revealed only when the round ends.
66 pub crash_point: f64,
67 pub started_at: f64,
68 /// `running`, `cashed`, `crashed` or `forfeit`.
69 pub phase: String,
70 pub result: String,
71 /// Live while running; frozen at the final value afterwards.
72 pub multiplier: f64,
73 pub payout: u64,
74}
75
76impl CrashRound {
77 pub fn new(bet: u64, crash_point: f64, started_at: f64) -> Self {
78 Self {
79 bet,
80 crash_point,
81 started_at,
82 phase: "running".to_string(),
83 result: "playing".to_string(),
84 multiplier: 1.0,
85 payout: 0,
86 }
87 }
88
89 pub fn is_running(&self) -> bool {
90 self.phase == "running"
91 }
92
93 pub fn cash_out(&mut self, multiplier: f64) -> u64 {
94 self.multiplier = multiplier;
95 self.payout = payout(self.bet, multiplier);
96 self.phase = "cashed".to_string();
97 self.result = "cashed".to_string();
98 self.payout
99 }
100
101 /// The round ended before the player could cash out.
102 pub fn crash(&mut self) {
103 self.multiplier = self.crash_point;
104 self.payout = 0;
105 self.phase = "crashed".to_string();
106 self.result = "crashed".to_string();
107 }
108
109 pub fn forfeit(&mut self) {
110 self.payout = 0;
111 self.phase = "forfeit".to_string();
112 self.result = "forfeit".to_string();
113 }
114
115 pub fn state(&self, session_id: &str, balance: i64) -> CrashState {
116 CrashState {
117 session_id: session_id.to_string(),
118 phase: self.phase.clone(),
119 bet: self.bet,
120 balance,
121 multiplier: self.multiplier,
122 potential: payout(self.bet, self.multiplier),
123 crash_point: if self.is_running() {
124 None
125 } else {
126 Some(self.crash_point)
127 },
128 payout: self.payout,
129 result: self.result.clone(),
130 }
131 }
132
133 pub fn to_json(&self) -> String {
134 serde_json::to_string(self).unwrap_or_default()
135 }
136
137 pub fn from_json(raw: &str) -> Result<Self, serde_json::Error> {
138 serde_json::from_str(raw)
139 }
140}
141
142/// Crash-point generator. Tests queue fixed uniforms; production draws random
143/// ones. The visible crash point stays hidden until `poll` or cashout ends the
144/// round.
145pub struct CrashGame {
146 uniforms: VecDeque<f64>,
147}
148
149impl CrashGame {
150 pub fn new() -> Self {
151 Self {
152 uniforms: VecDeque::new(),
153 }
154 }
155
156 pub fn with_uniform(uniform: f64) -> Self {
157 Self::with_uniforms([uniform])
158 }
159
160 pub fn with_uniforms(uniforms: impl IntoIterator<Item = f64>) -> Self {
161 Self {
162 uniforms: uniforms.into_iter().collect(),
163 }
164 }
165
166 pub fn next_crash_point(&mut self) -> f64 {
167 let uniform = self
168 .uniforms
169 .pop_front()
170 .unwrap_or_else(rand::random::<f64>);
171 crash_point_from_uniform(uniform)
172 }
173}
174
175impl Game for CrashGame {
176 fn game_type(&self) -> &'static str {
177 "crash"
178 }
179
180 fn get_name(&self) -> &'static str {
181 "Crash"
182 }
183
184 fn get_description(&self) -> &'static str {
185 "Crash: cash out before the multiplier crashes."
186 }
187
188 fn prepare_state(&self, _username: &str, bet_amount: u64) -> GameState {
189 let mut data = HashMap::new();
190 data.insert("status".to_string(), "ready".to_string());
191 GameState {
192 session_id: String::new(),
193 game_type: "crash".to_string(),
194 bet_amount,
195 phase: GamePhase::WaitingToStart,
196 data,
197 }
198 }
199
200 fn process_action(
201 &self,
202 _state: &GameState,
203 _action: &str,
204 _params: &[&str],
205 ) -> Result<GameState, GameError> {
206 Err(GameError::InvalidAction(
207 "Crash uses CRASH_CASHOUT and CRASH_FORFEIT".to_string(),
208 ))
209 }
210
211 fn calculate_result(&self, _state: &GameState) -> Result<GameResult, GameError> {
212 Err(GameError::InternalError(
213 "Crash settles through CRASH_CASHOUT".to_string(),
214 ))
215 }
216}
diff --git a/src/server/games/mod.rs b/src/server/games/mod.rs
index 07c5772..5de9eef 100644
--- a/src/server/games/mod.rs
+++ b/src/server/games/mod.rs
@@ -1,4 +1,5 @@
1pub mod blackjack; 1pub mod blackjack;
2pub mod crash;
2pub mod registry; 3pub mod registry;
3pub mod roulette; 4pub mod roulette;
4pub mod session; 5pub mod session;
diff --git a/src/server/games/registry.rs b/src/server/games/registry.rs
index 7ff8fbf..7a8b605 100644
--- a/src/server/games/registry.rs
+++ b/src/server/games/registry.rs
@@ -1,6 +1,7 @@
1#![allow(dead_code)] 1#![allow(dead_code)]
2 2
3use super::blackjack::BlackjackGame; 3use super::blackjack::BlackjackGame;
4use super::crash::CrashGame;
4use super::roulette::RouletteGame; 5use super::roulette::RouletteGame;
5use super::slots::SlotsGame; 6use super::slots::SlotsGame;
6use super::traits::Game; 7use super::traits::Game;
@@ -25,6 +26,7 @@ impl GameRegistry {
25 registry.register_game(SlotsGame::new()); 26 registry.register_game(SlotsGame::new());
26 registry.register_game(RouletteGame::new()); 27 registry.register_game(RouletteGame::new());
27 registry.register_game(BlackjackGame::new()); 28 registry.register_game(BlackjackGame::new());
29 registry.register_game(CrashGame::new());
28 registry 30 registry
29 } 31 }
30 32