diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/core.rs | 187 | ||||
| -rw-r--r-- | src/server/database.rs | 81 | ||||
| -rw-r--r-- | src/server/games/blackjack.rs | 274 | ||||
| -rw-r--r-- | src/server/games/mod.rs | 1 | ||||
| -rw-r--r-- | src/server/games/registry.rs | 2 | ||||
| -rw-r--r-- | src/server/games/session.rs | 1 |
6 files changed, 544 insertions, 2 deletions
diff --git a/src/server/core.rs b/src/server/core.rs index 2ce8b2b..cf8c4b9 100644 --- a/src/server/core.rs +++ b/src/server/core.rs | |||
| @@ -1,10 +1,13 @@ | |||
| 1 | use bcrypt::{hash, verify, DEFAULT_COST}; | 1 | use bcrypt::{hash, verify, DEFAULT_COST}; |
| 2 | use rusqlite::Connection; | 2 | use rusqlite::Connection; |
| 3 | use std::collections::HashMap; | ||
| 3 | 4 | ||
| 5 | use crate::blackjack::BlackjackState; | ||
| 4 | use crate::server::database; | 6 | use crate::server::database; |
| 7 | use crate::server::games::blackjack::{BlackjackGame, BlackjackHand}; | ||
| 5 | use crate::server::games::registry::GameRegistry; | 8 | use crate::server::games::registry::GameRegistry; |
| 6 | use crate::server::games::session::SessionManager; | 9 | use crate::server::games::session::SessionManager; |
| 7 | use crate::server::games::traits::GameError; | 10 | use crate::server::games::traits::{GameError, GamePhase, GameState}; |
| 8 | 11 | ||
| 9 | #[derive(Debug, Default)] | 12 | #[derive(Debug, Default)] |
| 10 | pub struct ConnectionState { | 13 | pub struct ConnectionState { |
| @@ -22,6 +25,7 @@ pub struct ServerCore { | |||
| 22 | sessions: SessionManager, | 25 | sessions: SessionManager, |
| 23 | registry: GameRegistry, | 26 | registry: GameRegistry, |
| 24 | bcrypt_cost: u32, | 27 | bcrypt_cost: u32, |
| 28 | blackjack: BlackjackGame, | ||
| 25 | } | 29 | } |
| 26 | 30 | ||
| 27 | impl ServerCore { | 31 | impl ServerCore { |
| @@ -39,9 +43,16 @@ impl ServerCore { | |||
| 39 | sessions: SessionManager::new(), | 43 | sessions: SessionManager::new(), |
| 40 | registry, | 44 | registry, |
| 41 | bcrypt_cost, | 45 | bcrypt_cost, |
| 46 | blackjack: BlackjackGame::new(), | ||
| 42 | } | 47 | } |
| 43 | } | 48 | } |
| 44 | 49 | ||
| 50 | /// Test/utility hook: use a fixed shoe for the next hand(s). | ||
| 51 | pub fn with_blackjack_deck(mut self, deck: Vec<String>) -> Self { | ||
| 52 | self.blackjack = BlackjackGame::with_deck(deck); | ||
| 53 | self | ||
| 54 | } | ||
| 55 | |||
| 45 | pub fn open(path: &str) -> rusqlite::Result<Self> { | 56 | pub fn open(path: &str) -> rusqlite::Result<Self> { |
| 46 | let conn = Connection::open(path)?; | 57 | let conn = Connection::open(path)?; |
| 47 | database::init_db(&conn); | 58 | database::init_db(&conn); |
| @@ -85,13 +96,23 @@ impl ServerCore { | |||
| 85 | } | 96 | } |
| 86 | } | 97 | } |
| 87 | "PLAY_START" => { | 98 | "PLAY_START" => { |
| 88 | if parts.len() != 3 && parts.len() != 4 { | 99 | if parts.len() == 3 && parts[1] == "blackjack" { |
| 100 | self.handle_blackjack_start(state, parts[2]) | ||
| 101 | } else if parts.len() != 3 && parts.len() != 4 { | ||
| 89 | "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount> [payload]" | 102 | "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount> [payload]" |
| 90 | .to_string() | 103 | .to_string() |
| 91 | } else { | 104 | } else { |
| 92 | self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied()) | 105 | self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied()) |
| 93 | } | 106 | } |
| 94 | } | 107 | } |
| 108 | "BLACKJACK_ACTION" => { | ||
| 109 | if parts.len() != 3 { | ||
| 110 | "ERROR Invalid BLACKJACK_ACTION format. Usage: BLACKJACK_ACTION <session> <action>" | ||
| 111 | .to_string() | ||
| 112 | } else { | ||
| 113 | self.handle_blackjack_action(state, parts[1], parts[2]) | ||
| 114 | } | ||
| 115 | } | ||
| 95 | "REGISTER" => { | 116 | "REGISTER" => { |
| 96 | if parts.len() != 3 { | 117 | if parts.len() != 3 { |
| 97 | "ERROR Invalid REGISTER format".to_string() | 118 | "ERROR Invalid REGISTER format".to_string() |
| @@ -182,6 +203,158 @@ impl ServerCore { | |||
| 182 | } | 203 | } |
| 183 | } | 204 | } |
| 184 | 205 | ||
| 206 | fn handle_blackjack_start(&mut self, state: &ConnectionState, bet_str: &str) -> String { | ||
| 207 | let Some(user) = state.user.as_deref() else { | ||
| 208 | return "ERROR Not logged in".to_string(); | ||
| 209 | }; | ||
| 210 | if self | ||
| 211 | .sessions | ||
| 212 | .get_user_sessions(user) | ||
| 213 | .iter() | ||
| 214 | .any(|session| session.game_type == "blackjack") | ||
| 215 | { | ||
| 216 | return "ERROR Blackjack hand already in progress".to_string(); | ||
| 217 | } | ||
| 218 | let bet: i64 = match bet_str.parse() { | ||
| 219 | Ok(bet) => bet, | ||
| 220 | Err(_) => return "ERROR Invalid bet amount".to_string(), | ||
| 221 | }; | ||
| 222 | if bet <= 0 { | ||
| 223 | return "ERROR Invalid bet amount".to_string(); | ||
| 224 | } | ||
| 225 | let Some(balance) = database::get_balance(&self.conn, user) else { | ||
| 226 | return "ERROR Could not retrieve balance".to_string(); | ||
| 227 | }; | ||
| 228 | if balance < bet { | ||
| 229 | return "ERROR Insufficient balance".to_string(); | ||
| 230 | } | ||
| 231 | let balance = match database::debit(&mut self.conn, user, bet) { | ||
| 232 | Ok(balance) => balance, | ||
| 233 | Err(_) => return "ERROR Insufficient balance".to_string(), | ||
| 234 | }; | ||
| 235 | |||
| 236 | let hand = self.blackjack.deal(bet as u64); | ||
| 237 | let session_id = self.create_blackjack_session(user, &hand); | ||
| 238 | self.blackjack_response(user, &session_id, hand, balance) | ||
| 239 | } | ||
| 240 | |||
| 241 | fn create_blackjack_session(&mut self, user: &str, hand: &BlackjackHand) -> String { | ||
| 242 | let mut data = HashMap::new(); | ||
| 243 | data.insert("hand".to_string(), hand.to_json()); | ||
| 244 | let game_state = GameState { | ||
| 245 | session_id: String::new(), | ||
| 246 | game_type: "blackjack".to_string(), | ||
| 247 | bet_amount: hand.bet, | ||
| 248 | phase: GamePhase::Playing, | ||
| 249 | data, | ||
| 250 | }; | ||
| 251 | self.sessions | ||
| 252 | .create_session(user, "blackjack".to_string(), game_state) | ||
| 253 | } | ||
| 254 | |||
| 255 | fn handle_blackjack_action( | ||
| 256 | &mut self, | ||
| 257 | state: &ConnectionState, | ||
| 258 | session_id: &str, | ||
| 259 | action: &str, | ||
| 260 | ) -> String { | ||
| 261 | let Some(user) = state.user.as_deref() else { | ||
| 262 | return "ERROR Not logged in".to_string(); | ||
| 263 | }; | ||
| 264 | let Ok(session) = self.sessions.get_session(session_id) else { | ||
| 265 | return "ERROR Session not found".to_string(); | ||
| 266 | }; | ||
| 267 | if session.username != user || session.game_type != "blackjack" { | ||
| 268 | return "ERROR Invalid session".to_string(); | ||
| 269 | } | ||
| 270 | let Some(raw) = session.state.data.get("hand") else { | ||
| 271 | return "ERROR Missing hand".to_string(); | ||
| 272 | }; | ||
| 273 | let Ok(mut hand) = BlackjackHand::from_json(raw) else { | ||
| 274 | return "ERROR Invalid hand".to_string(); | ||
| 275 | }; | ||
| 276 | if hand.is_done() { | ||
| 277 | return "ERROR Hand is over".to_string(); | ||
| 278 | } | ||
| 279 | |||
| 280 | let balance = database::get_balance(&self.conn, user).unwrap_or(0); | ||
| 281 | match action { | ||
| 282 | "hit" => { | ||
| 283 | if let Err(error) = hand.hit() { | ||
| 284 | return format!("ERROR {error}"); | ||
| 285 | } | ||
| 286 | } | ||
| 287 | "stand" => hand.stand(), | ||
| 288 | "double" => { | ||
| 289 | if hand.player.len() != 2 { | ||
| 290 | return "ERROR Double is only allowed on the first two cards".to_string(); | ||
| 291 | } | ||
| 292 | if database::debit(&mut self.conn, user, hand.base_bet as i64).is_err() { | ||
| 293 | return "ERROR Insufficient balance".to_string(); | ||
| 294 | } | ||
| 295 | if let Err(error) = hand.double() { | ||
| 296 | let _ = database::credit(&mut self.conn, user, hand.base_bet as i64); | ||
| 297 | return format!("ERROR {error}"); | ||
| 298 | } | ||
| 299 | } | ||
| 300 | "cancel" => { | ||
| 301 | let refund = session.bet_amount as i64; | ||
| 302 | let balance = database::credit(&mut self.conn, user, refund).unwrap_or(balance); | ||
| 303 | let _ = self.sessions.remove_session(session_id); | ||
| 304 | hand.cancel(); | ||
| 305 | let state = hand.state(session_id, balance); | ||
| 306 | return blackjack_wire(&state); | ||
| 307 | } | ||
| 308 | _ => return "ERROR Unknown action".to_string(), | ||
| 309 | } | ||
| 310 | |||
| 311 | let balance = database::get_balance(&self.conn, user).unwrap_or(balance); | ||
| 312 | self.blackjack_response(user, session_id, hand, balance) | ||
| 313 | } | ||
| 314 | |||
| 315 | fn blackjack_response( | ||
| 316 | &mut self, | ||
| 317 | user: &str, | ||
| 318 | session_id: &str, | ||
| 319 | hand: BlackjackHand, | ||
| 320 | balance: i64, | ||
| 321 | ) -> String { | ||
| 322 | if hand.is_done() { | ||
| 323 | let state = hand.state(session_id, balance); | ||
| 324 | let details = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string()); | ||
| 325 | let balance = match database::settle_play( | ||
| 326 | &mut self.conn, | ||
| 327 | session_id, | ||
| 328 | user, | ||
| 329 | "blackjack", | ||
| 330 | hand.bet as i64, | ||
| 331 | hand.winnings as i64, | ||
| 332 | &details, | ||
| 333 | ) { | ||
| 334 | Ok(balance) => balance, | ||
| 335 | Err(error) => { | ||
| 336 | eprintln!("Blackjack settlement failed for {user}: {error}"); | ||
| 337 | let _ = self.sessions.remove_session(session_id); | ||
| 338 | return "ERROR Failed to update balance".to_string(); | ||
| 339 | } | ||
| 340 | }; | ||
| 341 | let _ = self.sessions.remove_session(session_id); | ||
| 342 | let state = hand.state(session_id, balance); | ||
| 343 | blackjack_wire(&state) | ||
| 344 | } else { | ||
| 345 | if let Ok(mut session) = self.sessions.get_session(session_id) { | ||
| 346 | session.state.bet_amount = hand.bet; | ||
| 347 | session | ||
| 348 | .state | ||
| 349 | .data | ||
| 350 | .insert("hand".to_string(), hand.to_json()); | ||
| 351 | let _ = self.sessions.update_session(session_id, session.state); | ||
| 352 | } | ||
| 353 | let state = hand.state(session_id, balance); | ||
| 354 | blackjack_wire(&state) | ||
| 355 | } | ||
| 356 | } | ||
| 357 | |||
| 185 | fn handle_play_start( | 358 | fn handle_play_start( |
| 186 | &mut self, | 359 | &mut self, |
| 187 | state: &ConnectionState, | 360 | state: &ConnectionState, |
| @@ -311,6 +484,9 @@ impl ServerCore { | |||
| 311 | pub fn disconnect(&mut self, state: &ConnectionState) { | 484 | pub fn disconnect(&mut self, state: &ConnectionState) { |
| 312 | if let Some(user) = state.user.as_deref() { | 485 | if let Some(user) = state.user.as_deref() { |
| 313 | for session in self.sessions.get_user_sessions(user) { | 486 | for session in self.sessions.get_user_sessions(user) { |
| 487 | if session.game_type == "blackjack" { | ||
| 488 | let _ = database::credit(&mut self.conn, user, session.bet_amount as i64); | ||
| 489 | } | ||
| 314 | let _ = self.sessions.remove_session(&session.session_id); | 490 | let _ = self.sessions.remove_session(&session.session_id); |
| 315 | } | 491 | } |
| 316 | } | 492 | } |
| @@ -339,3 +515,10 @@ fn game_error(error: GameError) -> String { | |||
| 339 | GameError::InternalError(message) => format!("ERROR {message}"), | 515 | GameError::InternalError(message) => format!("ERROR {message}"), |
| 340 | } | 516 | } |
| 341 | } | 517 | } |
| 518 | |||
| 519 | fn blackjack_wire(state: &BlackjackState) -> String { | ||
| 520 | format!( | ||
| 521 | "BLACKJACK_STATE {}", | ||
| 522 | serde_json::to_string(state).unwrap_or_else(|_| "{}".to_string()) | ||
| 523 | ) | ||
| 524 | } | ||
diff --git a/src/server/database.rs b/src/server/database.rs index 2a1fc40..99e0dde 100644 --- a/src/server/database.rs +++ b/src/server/database.rs | |||
| @@ -177,3 +177,84 @@ pub fn count_plays(conn: &Connection, username: &str) -> i64 { | |||
| 177 | ) | 177 | ) |
| 178 | .unwrap_or(0) | 178 | .unwrap_or(0) |
| 179 | } | 179 | } |
| 180 | |||
| 181 | /// Reserve a bet before a multi-step hand starts. | ||
| 182 | pub fn debit(conn: &mut Connection, username: &str, amount: i64) -> rusqlite::Result<i64> { | ||
| 183 | let tx = conn.transaction()?; | ||
| 184 | let updated = tx.execute( | ||
| 185 | "UPDATE users SET balance = balance - ?1 | ||
| 186 | WHERE username = ?2 AND balance >= ?1", | ||
| 187 | params![amount, username], | ||
| 188 | )?; | ||
| 189 | if updated != 1 { | ||
| 190 | return Err(rusqlite::Error::QueryReturnedNoRows); | ||
| 191 | } | ||
| 192 | let balance = tx.query_row( | ||
| 193 | "SELECT balance FROM users WHERE username = ?1", | ||
| 194 | [username], | ||
| 195 | |row| row.get(0), | ||
| 196 | )?; | ||
| 197 | tx.commit()?; | ||
| 198 | Ok(balance) | ||
| 199 | } | ||
| 200 | |||
| 201 | /// Put a reserved bet back (disconnect or cancel). | ||
| 202 | pub fn credit(conn: &mut Connection, username: &str, amount: i64) -> rusqlite::Result<i64> { | ||
| 203 | let tx = conn.transaction()?; | ||
| 204 | tx.execute( | ||
| 205 | "UPDATE users SET balance = balance + ?1 WHERE username = ?2", | ||
| 206 | params![amount, username], | ||
| 207 | )?; | ||
| 208 | let balance = tx.query_row( | ||
| 209 | "SELECT balance FROM users WHERE username = ?1", | ||
| 210 | [username], | ||
| 211 | |row| row.get(0), | ||
| 212 | )?; | ||
| 213 | tx.commit()?; | ||
| 214 | Ok(balance) | ||
| 215 | } | ||
| 216 | |||
| 217 | /// Settle a hand whose bet was already reserved: credit winnings, write the | ||
| 218 | /// audit row, and return the new balance. | ||
| 219 | #[allow(clippy::too_many_arguments)] | ||
| 220 | pub fn settle_play( | ||
| 221 | conn: &mut Connection, | ||
| 222 | session_id: &str, | ||
| 223 | username: &str, | ||
| 224 | game_type: &str, | ||
| 225 | bet_amount: i64, | ||
| 226 | winnings: i64, | ||
| 227 | details: &str, | ||
| 228 | ) -> rusqlite::Result<i64> { | ||
| 229 | let tx = conn.transaction()?; | ||
| 230 | |||
| 231 | tx.execute( | ||
| 232 | "UPDATE users SET balance = balance + ?1 WHERE username = ?2", | ||
| 233 | params![winnings, username], | ||
| 234 | )?; | ||
| 235 | |||
| 236 | let balance_after: i64 = tx.query_row( | ||
| 237 | "SELECT balance FROM users WHERE username = ?1", | ||
| 238 | [username], | ||
| 239 | |row| row.get(0), | ||
| 240 | )?; | ||
| 241 | |||
| 242 | tx.execute( | ||
| 243 | "INSERT INTO plays | ||
| 244 | (session_id, username, game_type, bet_amount, winnings, | ||
| 245 | balance_after, details) | ||
| 246 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", | ||
| 247 | params![ | ||
| 248 | session_id, | ||
| 249 | username, | ||
| 250 | game_type, | ||
| 251 | bet_amount, | ||
| 252 | winnings, | ||
| 253 | balance_after, | ||
| 254 | details | ||
| 255 | ], | ||
| 256 | )?; | ||
| 257 | |||
| 258 | tx.commit()?; | ||
| 259 | Ok(balance_after) | ||
| 260 | } | ||
diff --git a/src/server/games/blackjack.rs b/src/server/games/blackjack.rs new file mode 100644 index 0000000..9505fed --- /dev/null +++ b/src/server/games/blackjack.rs | |||
| @@ -0,0 +1,274 @@ | |||
| 1 | use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; | ||
| 2 | use crate::blackjack::{ | ||
| 3 | card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState, | ||
| 4 | }; | ||
| 5 | use rand::seq::SliceRandom; | ||
| 6 | use rand::thread_rng; | ||
| 7 | use serde::{Deserialize, Serialize}; | ||
| 8 | use std::collections::HashMap; | ||
| 9 | |||
| 10 | pub struct BlackjackGame { | ||
| 11 | fixed_deck: Option<Vec<String>>, | ||
| 12 | } | ||
| 13 | |||
| 14 | impl BlackjackGame { | ||
| 15 | pub fn new() -> Self { | ||
| 16 | Self { fixed_deck: None } | ||
| 17 | } | ||
| 18 | |||
| 19 | pub fn with_deck(deck: Vec<String>) -> Self { | ||
| 20 | Self { | ||
| 21 | fixed_deck: Some(deck), | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | fn shoe(&self) -> Vec<String> { | ||
| 26 | if let Some(deck) = &self.fixed_deck { | ||
| 27 | return deck.clone(); | ||
| 28 | } | ||
| 29 | let mut shoe = fresh_shoe(); | ||
| 30 | shoe.shuffle(&mut thread_rng()); | ||
| 31 | shoe | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn deal(&self, bet: u64) -> BlackjackHand { | ||
| 35 | let mut hand = BlackjackHand::new(self.shoe(), bet); | ||
| 36 | if is_blackjack(&hand.player) || is_blackjack(&hand.dealer) { | ||
| 37 | hand.settle(); | ||
| 38 | } | ||
| 39 | hand | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 44 | pub struct BlackjackHand { | ||
| 45 | pub shoe: Vec<String>, | ||
| 46 | pub player: Vec<String>, | ||
| 47 | pub dealer: Vec<String>, | ||
| 48 | pub phase: String, | ||
| 49 | pub base_bet: u64, | ||
| 50 | pub bet: u64, | ||
| 51 | pub result: String, | ||
| 52 | pub winnings: u64, | ||
| 53 | } | ||
| 54 | |||
| 55 | impl BlackjackHand { | ||
| 56 | fn new(shoe: Vec<String>, bet: u64) -> Self { | ||
| 57 | let mut hand = Self { | ||
| 58 | shoe, | ||
| 59 | player: Vec::new(), | ||
| 60 | dealer: Vec::new(), | ||
| 61 | phase: "player".to_string(), | ||
| 62 | base_bet: bet, | ||
| 63 | bet, | ||
| 64 | result: "playing".to_string(), | ||
| 65 | winnings: 0, | ||
| 66 | }; | ||
| 67 | |||
| 68 | let card = hand.draw(); | ||
| 69 | hand.player.push(card); | ||
| 70 | let card = hand.draw(); | ||
| 71 | hand.dealer.push(card); | ||
| 72 | let card = hand.draw(); | ||
| 73 | hand.player.push(card); | ||
| 74 | let card = hand.draw(); | ||
| 75 | hand.dealer.push(card); | ||
| 76 | |||
| 77 | hand | ||
| 78 | } | ||
| 79 | |||
| 80 | fn draw(&mut self) -> String { | ||
| 81 | self.shoe.remove(0) | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn player_total(&self) -> u8 { | ||
| 85 | hand_total(&self.player) | ||
| 86 | } | ||
| 87 | |||
| 88 | pub fn dealer_total(&self) -> u8 { | ||
| 89 | hand_total(&self.dealer) | ||
| 90 | } | ||
| 91 | |||
| 92 | pub fn is_done(&self) -> bool { | ||
| 93 | self.phase == "done" | ||
| 94 | } | ||
| 95 | |||
| 96 | pub fn hit(&mut self) -> Result<(), String> { | ||
| 97 | if self.is_done() { | ||
| 98 | return Err("Hand is over".to_string()); | ||
| 99 | } | ||
| 100 | let card = self.draw(); | ||
| 101 | self.player.push(card); | ||
| 102 | let total = self.player_total(); | ||
| 103 | if total > 21 { | ||
| 104 | self.settle(); | ||
| 105 | } else if total == 21 { | ||
| 106 | self.dealer_play(); | ||
| 107 | self.settle(); | ||
| 108 | } | ||
| 109 | Ok(()) | ||
| 110 | } | ||
| 111 | |||
| 112 | pub fn stand(&mut self) { | ||
| 113 | if self.is_done() { | ||
| 114 | return; | ||
| 115 | } | ||
| 116 | self.dealer_play(); | ||
| 117 | self.settle(); | ||
| 118 | } | ||
| 119 | |||
| 120 | /// Caller must debit the extra `base_bet` before calling this. | ||
| 121 | pub fn double(&mut self) -> Result<(), String> { | ||
| 122 | if self.is_done() { | ||
| 123 | return Err("Hand is over".to_string()); | ||
| 124 | } | ||
| 125 | if self.player.len() != 2 { | ||
| 126 | return Err("Double is only allowed on the first two cards".to_string()); | ||
| 127 | } | ||
| 128 | self.bet = self.bet.saturating_add(self.base_bet); | ||
| 129 | let card = self.draw(); | ||
| 130 | self.player.push(card); | ||
| 131 | self.dealer_play(); | ||
| 132 | self.settle(); | ||
| 133 | Ok(()) | ||
| 134 | } | ||
| 135 | |||
| 136 | pub fn cancel(&mut self) { | ||
| 137 | self.phase = "done".to_string(); | ||
| 138 | self.result = "cancelled".to_string(); | ||
| 139 | self.winnings = 0; | ||
| 140 | } | ||
| 141 | |||
| 142 | fn dealer_play(&mut self) { | ||
| 143 | while self.dealer_total() < 17 { | ||
| 144 | let card = self.draw(); | ||
| 145 | self.dealer.push(card); | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | fn settle(&mut self) { | ||
| 150 | if self.is_done() { | ||
| 151 | return; | ||
| 152 | } | ||
| 153 | self.phase = "done".to_string(); | ||
| 154 | |||
| 155 | let player = self.player_total(); | ||
| 156 | let dealer = self.dealer_total(); | ||
| 157 | let player_blackjack = is_blackjack(&self.player); | ||
| 158 | let dealer_blackjack = is_blackjack(&self.dealer); | ||
| 159 | |||
| 160 | self.result = if player > 21 { | ||
| 161 | "bust".to_string() | ||
| 162 | } else if player_blackjack && !dealer_blackjack { | ||
| 163 | "blackjack".to_string() | ||
| 164 | } else if dealer_blackjack && !player_blackjack { | ||
| 165 | "lose".to_string() | ||
| 166 | } else if dealer > 21 { | ||
| 167 | "win".to_string() | ||
| 168 | } else if player == dealer { | ||
| 169 | "push".to_string() | ||
| 170 | } else if player > dealer { | ||
| 171 | "win".to_string() | ||
| 172 | } else { | ||
| 173 | "lose".to_string() | ||
| 174 | }; | ||
| 175 | |||
| 176 | self.winnings = match self.result.as_str() { | ||
| 177 | "push" => self.bet, | ||
| 178 | "blackjack" => self.bet + (self.bet * 3) / 2, | ||
| 179 | "win" => self.bet * 2, | ||
| 180 | _ => 0, | ||
| 181 | }; | ||
| 182 | } | ||
| 183 | |||
| 184 | pub fn state(&self, session_id: &str, balance: i64) -> BlackjackState { | ||
| 185 | let player_total = self.player_total(); | ||
| 186 | let (dealer, dealer_total, actions) = if self.is_done() { | ||
| 187 | (self.dealer.clone(), self.dealer_total(), Vec::new()) | ||
| 188 | } else { | ||
| 189 | let visible = self | ||
| 190 | .dealer | ||
| 191 | .first() | ||
| 192 | .map(|card| card_value(card)) | ||
| 193 | .unwrap_or(0); | ||
| 194 | let mut actions = vec!["hit".to_string(), "stand".to_string()]; | ||
| 195 | if self.player.len() == 2 { | ||
| 196 | actions.push("double".to_string()); | ||
| 197 | } | ||
| 198 | ( | ||
| 199 | vec![ | ||
| 200 | self.dealer.first().cloned().unwrap_or_default(), | ||
| 201 | "?".to_string(), | ||
| 202 | ], | ||
| 203 | visible, | ||
| 204 | actions, | ||
| 205 | ) | ||
| 206 | }; | ||
| 207 | |||
| 208 | BlackjackState { | ||
| 209 | session_id: session_id.to_string(), | ||
| 210 | phase: self.phase.clone(), | ||
| 211 | player: self.player.clone(), | ||
| 212 | dealer, | ||
| 213 | player_total, | ||
| 214 | dealer_total, | ||
| 215 | bet: self.bet, | ||
| 216 | base_bet: self.base_bet, | ||
| 217 | balance, | ||
| 218 | actions, | ||
| 219 | result: self.result.clone(), | ||
| 220 | winnings: self.winnings, | ||
| 221 | } | ||
| 222 | } | ||
| 223 | |||
| 224 | pub fn to_json(&self) -> String { | ||
| 225 | serde_json::to_string(self).unwrap_or_default() | ||
| 226 | } | ||
| 227 | |||
| 228 | pub fn from_json(raw: &str) -> Result<Self, serde_json::Error> { | ||
| 229 | serde_json::from_str(raw) | ||
| 230 | } | ||
| 231 | } | ||
| 232 | |||
| 233 | impl Game for BlackjackGame { | ||
| 234 | fn game_type(&self) -> &'static str { | ||
| 235 | "blackjack" | ||
| 236 | } | ||
| 237 | |||
| 238 | fn get_name(&self) -> &'static str { | ||
| 239 | "Blackjack" | ||
| 240 | } | ||
| 241 | |||
| 242 | fn get_description(&self) -> &'static str { | ||
| 243 | "Blackjack: hit, stand or double. Dealer stands on all 17s." | ||
| 244 | } | ||
| 245 | |||
| 246 | fn prepare_state(&self, _username: &str, bet_amount: u64) -> GameState { | ||
| 247 | let mut data = HashMap::new(); | ||
| 248 | data.insert("status".to_string(), "ready".to_string()); | ||
| 249 | GameState { | ||
| 250 | session_id: String::new(), | ||
| 251 | game_type: "blackjack".to_string(), | ||
| 252 | bet_amount, | ||
| 253 | phase: GamePhase::WaitingToStart, | ||
| 254 | data, | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | fn process_action( | ||
| 259 | &self, | ||
| 260 | _state: &GameState, | ||
| 261 | _action: &str, | ||
| 262 | _params: &[&str], | ||
| 263 | ) -> Result<GameState, GameError> { | ||
| 264 | Err(GameError::InvalidAction( | ||
| 265 | "Blackjack uses BLACKJACK_ACTION".to_string(), | ||
| 266 | )) | ||
| 267 | } | ||
| 268 | |||
| 269 | fn calculate_result(&self, _state: &GameState) -> Result<GameResult, GameError> { | ||
| 270 | Err(GameError::InternalError( | ||
| 271 | "Blackjack settles through BLACKJACK_ACTION".to_string(), | ||
| 272 | )) | ||
| 273 | } | ||
| 274 | } | ||
diff --git a/src/server/games/mod.rs b/src/server/games/mod.rs index 35728a1..07c5772 100644 --- a/src/server/games/mod.rs +++ b/src/server/games/mod.rs | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | pub mod blackjack; | ||
| 1 | pub mod registry; | 2 | pub mod registry; |
| 2 | pub mod roulette; | 3 | pub mod roulette; |
| 3 | pub mod session; | 4 | pub mod session; |
diff --git a/src/server/games/registry.rs b/src/server/games/registry.rs index 51d446b..7ff8fbf 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 | ||
| 3 | use super::blackjack::BlackjackGame; | ||
| 3 | use super::roulette::RouletteGame; | 4 | use super::roulette::RouletteGame; |
| 4 | use super::slots::SlotsGame; | 5 | use super::slots::SlotsGame; |
| 5 | use super::traits::Game; | 6 | use super::traits::Game; |
| @@ -23,6 +24,7 @@ impl GameRegistry { | |||
| 23 | let registry = Self::new(); | 24 | let registry = Self::new(); |
| 24 | registry.register_game(SlotsGame::new()); | 25 | registry.register_game(SlotsGame::new()); |
| 25 | registry.register_game(RouletteGame::new()); | 26 | registry.register_game(RouletteGame::new()); |
| 27 | registry.register_game(BlackjackGame::new()); | ||
| 26 | registry | 28 | registry |
| 27 | } | 29 | } |
| 28 | 30 | ||
diff --git a/src/server/games/session.rs b/src/server/games/session.rs index a8410a9..916a45d 100644 --- a/src/server/games/session.rs +++ b/src/server/games/session.rs | |||
| @@ -51,6 +51,7 @@ impl SessionManager { | |||
| 51 | pub fn update_session(&self, session_id: &str, new_state: GameState) -> Result<(), GameError> { | 51 | pub fn update_session(&self, session_id: &str, new_state: GameState) -> Result<(), GameError> { |
| 52 | let mut sessions = self.sessions.lock().unwrap(); | 52 | let mut sessions = self.sessions.lock().unwrap(); |
| 53 | if let Some(session) = sessions.get_mut(session_id) { | 53 | if let Some(session) = sessions.get_mut(session_id) { |
| 54 | session.bet_amount = new_state.bet_amount; | ||
| 54 | session.state = new_state; | 55 | session.state = new_state; |
| 55 | Ok(()) | 56 | Ok(()) |
| 56 | } else { | 57 | } else { |
