summaryrefslogtreecommitdiffstats
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/core.rs77
-rw-r--r--src/server/games/mod.rs1
-rw-r--r--src/server/games/registry.rs2
-rw-r--r--src/server/games/roulette.rs151
4 files changed, 204 insertions, 27 deletions
diff --git a/src/server/core.rs b/src/server/core.rs
index 7231577..2ce8b2b 100644
--- a/src/server/core.rs
+++ b/src/server/core.rs
@@ -85,11 +85,11 @@ impl ServerCore {
85 } 85 }
86 } 86 }
87 "PLAY_START" => { 87 "PLAY_START" => {
88 if parts.len() != 3 { 88 if parts.len() != 3 && parts.len() != 4 {
89 "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount>" 89 "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount> [payload]"
90 .to_string() 90 .to_string()
91 } else { 91 } else {
92 self.handle_play_start(state, parts[1], parts[2]) 92 self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied())
93 } 93 }
94 } 94 }
95 "REGISTER" => { 95 "REGISTER" => {
@@ -187,6 +187,7 @@ impl ServerCore {
187 state: &ConnectionState, 187 state: &ConnectionState,
188 game_type: &str, 188 game_type: &str,
189 bet_amount_str: &str, 189 bet_amount_str: &str,
190 payload: Option<&str>,
190 ) -> String { 191 ) -> String {
191 let Some(user) = state.user.as_deref() else { 192 let Some(user) = state.user.as_deref() else {
192 return "ERROR Not logged in".to_string(); 193 return "ERROR Not logged in".to_string();
@@ -216,7 +217,8 @@ impl ServerCore {
216 } 217 }
217 218
218 let game_state = game.prepare_state(user, bet_amount as u64); 219 let game_state = game.prepare_state(user, bet_amount as u64);
219 let new_state = match game.process_action(&game_state, "spin", &[]) { 220 let params: Vec<&str> = payload.into_iter().collect();
221 let new_state = match game.process_action(&game_state, "spin", &params) {
220 Ok(state) => state, 222 Ok(state) => state,
221 Err(error) => return game_error(error), 223 Err(error) => return game_error(error),
222 }; 224 };
@@ -239,26 +241,6 @@ impl ServerCore {
239 } 241 }
240 }; 242 };
241 243
242 let reels = result
243 .result_data
244 .get("reels")
245 .cloned()
246 .unwrap_or_default();
247 let win_type = result
248 .result_data
249 .get("win_type")
250 .cloned()
251 .unwrap_or_default();
252 let win_lines = result
253 .result_data
254 .get("win_lines")
255 .cloned()
256 .unwrap_or_else(|| "-".to_string());
257 let win_cells = result
258 .result_data
259 .get("win_cells")
260 .cloned()
261 .unwrap_or_else(|| "-".to_string());
262 let details = 244 let details =
263 serde_json::to_string(&result.result_data).unwrap_or_else(|_| "{}".to_string()); 245 serde_json::to_string(&result.result_data).unwrap_or_else(|_| "{}".to_string());
264 246
@@ -273,9 +255,50 @@ impl ServerCore {
273 ) { 255 ) {
274 Ok(new_balance) => { 256 Ok(new_balance) => {
275 let _ = self.sessions.remove_session(&session_id); 257 let _ = self.sessions.remove_session(&session_id);
276 format!( 258 if game_type == "roulette" {
277 "GAME_RESULT {session_id} {winnings} {bet_amount} {new_balance} {reels} {win_type} {win_lines} {win_cells}" 259 let number = result
278 ) 260 .result_data
261 .get("number")
262 .cloned()
263 .unwrap_or_default();
264 let color = result
265 .result_data
266 .get("color")
267 .cloned()
268 .unwrap_or_default();
269 let bets = result
270 .result_data
271 .get("bets")
272 .cloned()
273 .unwrap_or_else(|| "{}".to_string());
274 format!(
275 "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}"
276 )
277 } else {
278 let reels = result
279 .result_data
280 .get("reels")
281 .cloned()
282 .unwrap_or_default();
283 let win_type = result
284 .result_data
285 .get("win_type")
286 .cloned()
287 .unwrap_or_default();
288 let win_lines = result
289 .result_data
290 .get("win_lines")
291 .cloned()
292 .unwrap_or_else(|| "-".to_string());
293 let win_cells = result
294 .result_data
295 .get("win_cells")
296 .cloned()
297 .unwrap_or_else(|| "-".to_string());
298 format!(
299 "GAME_RESULT {session_id} {winnings} {bet_amount} {new_balance} {reels} {win_type} {win_lines} {win_cells}"
300 )
301 }
279 } 302 }
280 Err(error) => { 303 Err(error) => {
281 let _ = self.sessions.remove_session(&session_id); 304 let _ = self.sessions.remove_session(&session_id);
diff --git a/src/server/games/mod.rs b/src/server/games/mod.rs
index 4c62760..35728a1 100644
--- a/src/server/games/mod.rs
+++ b/src/server/games/mod.rs
@@ -1,4 +1,5 @@
1pub mod registry; 1pub mod registry;
2pub mod roulette;
2pub mod session; 3pub mod session;
3pub mod slots; 4pub mod slots;
4pub mod traits; 5pub mod traits;
diff --git a/src/server/games/registry.rs b/src/server/games/registry.rs
index b7ed9d3..51d446b 100644
--- a/src/server/games/registry.rs
+++ b/src/server/games/registry.rs
@@ -1,5 +1,6 @@
1#![allow(dead_code)] 1#![allow(dead_code)]
2 2
3use super::roulette::RouletteGame;
3use super::slots::SlotsGame; 4use super::slots::SlotsGame;
4use super::traits::Game; 5use super::traits::Game;
5use std::collections::HashMap; 6use std::collections::HashMap;
@@ -21,6 +22,7 @@ impl GameRegistry {
21 pub fn with_defaults() -> Self { 22 pub fn with_defaults() -> Self {
22 let registry = Self::new(); 23 let registry = Self::new();
23 registry.register_game(SlotsGame::new()); 24 registry.register_game(SlotsGame::new());
25 registry.register_game(RouletteGame::new());
24 registry 26 registry
25 } 27 }
26 28
diff --git a/src/server/games/roulette.rs b/src/server/games/roulette.rs
new file mode 100644
index 0000000..7beb8c9
--- /dev/null
+++ b/src/server/games/roulette.rs
@@ -0,0 +1,151 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use crate::roulette::{color, Bets};
3use rand::Rng;
4use std::collections::HashMap;
5
6pub struct RouletteGame {
7 fixed_number: Option<u8>,
8}
9
10impl RouletteGame {
11 pub fn new() -> Self {
12 Self { fixed_number: None }
13 }
14
15 pub fn with_result(number: u8) -> Self {
16 Self {
17 fixed_number: Some(number),
18 }
19 }
20
21 fn spin(&self) -> u8 {
22 match self.fixed_number {
23 Some(number) => number,
24 None => rand::thread_rng().gen_range(0..37),
25 }
26 }
27}
28
29impl Game for RouletteGame {
30 fn game_type(&self) -> &'static str {
31 "roulette"
32 }
33
34 fn get_name(&self) -> &'static str {
35 "Roulette"
36 }
37
38 fn get_description(&self) -> &'static str {
39 "European roulette. Bet on red/black, even/odd, or a straight number."
40 }
41
42 fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState {
43 let mut data = HashMap::new();
44 data.insert("username".to_string(), username.to_string());
45 data.insert("status".to_string(), "ready".to_string());
46
47 GameState {
48 session_id: String::new(),
49 game_type: "roulette".to_string(),
50 bet_amount,
51 phase: GamePhase::WaitingToStart,
52 data,
53 }
54 }
55
56 fn process_action(
57 &self,
58 state: &GameState,
59 action: &str,
60 params: &[&str],
61 ) -> Result<GameState, GameError> {
62 if state.phase == GamePhase::Finished {
63 return Err(GameError::GameOver);
64 }
65
66 match action {
67 "spin" => {
68 if state.phase != GamePhase::WaitingToStart
69 && state.phase != GamePhase::Playing
70 {
71 return Err(GameError::InvalidAction(
72 "Cannot spin in current phase".to_string(),
73 ));
74 }
75
76 let payload = params
77 .first()
78 .ok_or(GameError::InvalidAction("Missing roulette bets".to_string()))?;
79 let bets: Bets = serde_json::from_str(payload)
80 .map_err(|error| GameError::InvalidAction(format!("Invalid bets: {error}")))?;
81
82 if bets.is_empty() {
83 return Err(GameError::InvalidBet);
84 }
85 if let Err(message) = bets.validate(state.bet_amount) {
86 return Err(GameError::InvalidAction(message));
87 }
88 if bets.total() != state.bet_amount {
89 return Err(GameError::InvalidBet);
90 }
91
92 let number = self.spin();
93 let bets_json = serde_json::to_string(&bets)
94 .map_err(|error| GameError::InternalError(error.to_string()))?;
95
96 let mut new_data = state.data.clone();
97 new_data.insert("status".to_string(), "spun".to_string());
98 new_data.insert("bets".to_string(), bets_json);
99 new_data.insert("number".to_string(), number.to_string());
100 new_data.insert("color".to_string(), color(number).to_string());
101
102 Ok(GameState {
103 session_id: state.session_id.clone(),
104 game_type: state.game_type.clone(),
105 bet_amount: state.bet_amount,
106 phase: GamePhase::Finished,
107 data: new_data,
108 })
109 }
110 _ => Err(GameError::InvalidAction(format!(
111 "Unknown action: {action}"
112 ))),
113 }
114 }
115
116 fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError> {
117 if state.phase != GamePhase::Finished {
118 return Err(GameError::InternalError("Game not finished".to_string()));
119 }
120
121 let number: u8 = state
122 .data
123 .get("number")
124 .ok_or(GameError::InternalError("No winning number".to_string()))?
125 .parse()
126 .map_err(|_| GameError::InternalError("Invalid winning number".to_string()))?;
127 let bets_json = state
128 .data
129 .get("bets")
130 .ok_or(GameError::InternalError("No bets".to_string()))?
131 .clone();
132 let bets: Bets = serde_json::from_str(&bets_json)
133 .map_err(|error| GameError::InternalError(format!("Invalid bets: {error}")))?;
134
135 let winnings = bets.winnings(number);
136
137 let mut result_data = HashMap::new();
138 result_data.insert("number".to_string(), number.to_string());
139 result_data.insert("color".to_string(), color(number).to_string());
140 result_data.insert("bets".to_string(), bets_json);
141
142 Ok(GameResult {
143 session_id: state.session_id.clone(),
144 game_type: state.game_type.clone(),
145 bet_amount: state.bet_amount,
146 winnings,
147 final_balance: 0,
148 result_data,
149 })
150 }
151}