From b5a52fc0312afdf5d5fbe53275dc88d6d881c2ff Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 18:22:40 +0100 Subject: v2.2.0: add blackjack with reserved bets and hit/stand/double --- src/server/core.rs | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 2 deletions(-) (limited to 'src/server/core.rs') 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 @@ use bcrypt::{hash, verify, DEFAULT_COST}; use rusqlite::Connection; +use std::collections::HashMap; +use crate::blackjack::BlackjackState; use crate::server::database; +use crate::server::games::blackjack::{BlackjackGame, BlackjackHand}; use crate::server::games::registry::GameRegistry; use crate::server::games::session::SessionManager; -use crate::server::games::traits::GameError; +use crate::server::games::traits::{GameError, GamePhase, GameState}; #[derive(Debug, Default)] pub struct ConnectionState { @@ -22,6 +25,7 @@ pub struct ServerCore { sessions: SessionManager, registry: GameRegistry, bcrypt_cost: u32, + blackjack: BlackjackGame, } impl ServerCore { @@ -39,9 +43,16 @@ impl ServerCore { sessions: SessionManager::new(), registry, bcrypt_cost, + blackjack: BlackjackGame::new(), } } + /// Test/utility hook: use a fixed shoe for the next hand(s). + pub fn with_blackjack_deck(mut self, deck: Vec) -> Self { + self.blackjack = BlackjackGame::with_deck(deck); + self + } + pub fn open(path: &str) -> rusqlite::Result { let conn = Connection::open(path)?; database::init_db(&conn); @@ -85,13 +96,23 @@ impl ServerCore { } } "PLAY_START" => { - if parts.len() != 3 && parts.len() != 4 { + if parts.len() == 3 && parts[1] == "blackjack" { + self.handle_blackjack_start(state, parts[2]) + } else if parts.len() != 3 && parts.len() != 4 { "ERROR Invalid PLAY_START format. Usage: PLAY_START [payload]" .to_string() } else { self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied()) } } + "BLACKJACK_ACTION" => { + if parts.len() != 3 { + "ERROR Invalid BLACKJACK_ACTION format. Usage: BLACKJACK_ACTION " + .to_string() + } else { + self.handle_blackjack_action(state, parts[1], parts[2]) + } + } "REGISTER" => { if parts.len() != 3 { "ERROR Invalid REGISTER format".to_string() @@ -182,6 +203,158 @@ impl ServerCore { } } + fn handle_blackjack_start(&mut self, state: &ConnectionState, bet_str: &str) -> String { + let Some(user) = state.user.as_deref() else { + return "ERROR Not logged in".to_string(); + }; + if self + .sessions + .get_user_sessions(user) + .iter() + .any(|session| session.game_type == "blackjack") + { + return "ERROR Blackjack hand already in progress".to_string(); + } + let bet: i64 = match bet_str.parse() { + Ok(bet) => bet, + Err(_) => return "ERROR Invalid bet amount".to_string(), + }; + if bet <= 0 { + return "ERROR Invalid bet amount".to_string(); + } + let Some(balance) = database::get_balance(&self.conn, user) else { + return "ERROR Could not retrieve balance".to_string(); + }; + if balance < bet { + return "ERROR Insufficient balance".to_string(); + } + let balance = match database::debit(&mut self.conn, user, bet) { + Ok(balance) => balance, + Err(_) => return "ERROR Insufficient balance".to_string(), + }; + + let hand = self.blackjack.deal(bet as u64); + let session_id = self.create_blackjack_session(user, &hand); + self.blackjack_response(user, &session_id, hand, balance) + } + + fn create_blackjack_session(&mut self, user: &str, hand: &BlackjackHand) -> String { + let mut data = HashMap::new(); + data.insert("hand".to_string(), hand.to_json()); + let game_state = GameState { + session_id: String::new(), + game_type: "blackjack".to_string(), + bet_amount: hand.bet, + phase: GamePhase::Playing, + data, + }; + self.sessions + .create_session(user, "blackjack".to_string(), game_state) + } + + fn handle_blackjack_action( + &mut self, + state: &ConnectionState, + session_id: &str, + action: &str, + ) -> String { + let Some(user) = state.user.as_deref() else { + return "ERROR Not logged in".to_string(); + }; + let Ok(session) = self.sessions.get_session(session_id) else { + return "ERROR Session not found".to_string(); + }; + if session.username != user || session.game_type != "blackjack" { + return "ERROR Invalid session".to_string(); + } + let Some(raw) = session.state.data.get("hand") else { + return "ERROR Missing hand".to_string(); + }; + let Ok(mut hand) = BlackjackHand::from_json(raw) else { + return "ERROR Invalid hand".to_string(); + }; + if hand.is_done() { + return "ERROR Hand is over".to_string(); + } + + let balance = database::get_balance(&self.conn, user).unwrap_or(0); + match action { + "hit" => { + if let Err(error) = hand.hit() { + return format!("ERROR {error}"); + } + } + "stand" => hand.stand(), + "double" => { + if hand.player.len() != 2 { + return "ERROR Double is only allowed on the first two cards".to_string(); + } + if database::debit(&mut self.conn, user, hand.base_bet as i64).is_err() { + return "ERROR Insufficient balance".to_string(); + } + if let Err(error) = hand.double() { + let _ = database::credit(&mut self.conn, user, hand.base_bet as i64); + return format!("ERROR {error}"); + } + } + "cancel" => { + let refund = session.bet_amount as i64; + let balance = database::credit(&mut self.conn, user, refund).unwrap_or(balance); + let _ = self.sessions.remove_session(session_id); + hand.cancel(); + let state = hand.state(session_id, balance); + return blackjack_wire(&state); + } + _ => return "ERROR Unknown action".to_string(), + } + + let balance = database::get_balance(&self.conn, user).unwrap_or(balance); + self.blackjack_response(user, session_id, hand, balance) + } + + fn blackjack_response( + &mut self, + user: &str, + session_id: &str, + hand: BlackjackHand, + balance: i64, + ) -> String { + if hand.is_done() { + let state = hand.state(session_id, balance); + let details = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string()); + let balance = match database::settle_play( + &mut self.conn, + session_id, + user, + "blackjack", + hand.bet as i64, + hand.winnings as i64, + &details, + ) { + Ok(balance) => balance, + Err(error) => { + eprintln!("Blackjack settlement failed for {user}: {error}"); + let _ = self.sessions.remove_session(session_id); + return "ERROR Failed to update balance".to_string(); + } + }; + let _ = self.sessions.remove_session(session_id); + let state = hand.state(session_id, balance); + blackjack_wire(&state) + } else { + if let Ok(mut session) = self.sessions.get_session(session_id) { + session.state.bet_amount = hand.bet; + session + .state + .data + .insert("hand".to_string(), hand.to_json()); + let _ = self.sessions.update_session(session_id, session.state); + } + let state = hand.state(session_id, balance); + blackjack_wire(&state) + } + } + fn handle_play_start( &mut self, state: &ConnectionState, @@ -311,6 +484,9 @@ impl ServerCore { pub fn disconnect(&mut self, state: &ConnectionState) { if let Some(user) = state.user.as_deref() { for session in self.sessions.get_user_sessions(user) { + if session.game_type == "blackjack" { + let _ = database::credit(&mut self.conn, user, session.bet_amount as i64); + } let _ = self.sessions.remove_session(&session.session_id); } } @@ -339,3 +515,10 @@ fn game_error(error: GameError) -> String { GameError::InternalError(message) => format!("ERROR {message}"), } } + +fn blackjack_wire(state: &BlackjackState) -> String { + format!( + "BLACKJACK_STATE {}", + serde_json::to_string(state).unwrap_or_else(|_| "{}".to_string()) + ) +} -- cgit v1.2.3