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, GamePhase, GameState}; #[derive(Debug, Default)] pub struct ConnectionState { user: Option, } impl ConnectionState { pub fn user(&self) -> Option<&str> { self.user.as_deref() } } pub struct ServerCore { conn: Connection, sessions: SessionManager, registry: GameRegistry, bcrypt_cost: u32, blackjack: BlackjackGame, } impl ServerCore { pub fn new(conn: Connection) -> Self { Self::with_registry(conn, GameRegistry::with_defaults(), DEFAULT_COST) } pub fn with_registry( conn: Connection, registry: GameRegistry, bcrypt_cost: u32, ) -> Self { Self { conn, 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); Ok(Self::new(conn)) } pub fn handle_message(&mut self, state: &mut ConnectionState, msg: &str) -> String { let parts: Vec<&str> = msg.splitn(5, ' ').collect(); match parts[0] { "GAME_LIST" => { let mut games = self.registry.list_games(); games.sort(); let info: Vec = games .iter() .filter_map(|game_type| { self.registry .get_game(game_type) .map(|game| format!("{}:{}", game_type, game.get_name())) }) .collect(); if info.is_empty() { "ERROR No games available".to_string() } else { format!("GAME_LIST {}", info.join(",")) } } "GAME_INFO" => { if parts.len() != 2 { "ERROR Invalid GAME_INFO format. Usage: GAME_INFO " .to_string() } else { match self.registry.get_game(parts[1]) { Some(game) => format!( "GAME_INFO {} {} {}", parts[1], game.get_name(), game.get_description() ), None => format!("ERROR Unknown game type: {}", parts[1]), } } } "PLAY_START" => { 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() } else { self.handle_register(parts[1], parts[2]) } } "AUTH" => { if parts.len() != 3 { "ERROR Invalid AUTH format".to_string() } else { self.handle_auth(state, parts[1], parts[2]) } } "PING" => "PONG".to_string(), "INFO" => self.handle_info(state), _ => msg.to_string(), } } fn handle_register(&mut self, username: &str, credential: &str) -> String { if database::get_hashed_password(&self.conn, username).is_some() { return "ERROR User already exists".to_string(); } let hashed = match hash(credential, self.bcrypt_cost) { Ok(hashed) => hashed, Err(error) => { eprintln!("bcrypt failed for {username}: {error}"); return "ERROR Could not hash password".to_string(); } }; match database::add_user(&self.conn, username, &hashed) { Ok(()) => format!("REGISTERED {}", username), Err(_) => "ERROR User already exists".to_string(), } } fn handle_auth( &mut self, state: &mut ConnectionState, username: &str, credential: &str, ) -> String { let Some(stored) = database::get_hashed_password(&self.conn, username) else { return "ERROR User not found".to_string(); }; let valid = if stored.starts_with("$2") { verify(credential, &stored).unwrap_or(false) } else if stored == credential { // Legacy unsalted SHA-256 row: accept once, then upgrade in place. match hash(credential, self.bcrypt_cost) { Ok(upgraded) => { if let Err(error) = database::set_hashed_password(&self.conn, username, &upgraded) { eprintln!("failed to upgrade password for {username}: {error}"); } } Err(error) => eprintln!("bcrypt failed for {username}: {error}"), } true } else { false }; if valid { state.user = Some(username.to_string()); format!("OK {}", username) } else { "ERROR Invalid credentials".to_string() } } fn handle_info(&self, state: &ConnectionState) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; match ( database::get_account_id(&self.conn, user), database::get_balance(&self.conn, user), ) { (Some(id), Some(balance)) => format!("INFO {} {} {}", user, id, balance), _ => "ERROR Error retrieving user info".to_string(), } } 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, game_type: &str, bet_amount_str: &str, payload: Option<&str>, ) -> String { let Some(user) = state.user.as_deref() else { return "ERROR Not logged in".to_string(); }; let bet_amount: i64 = match bet_amount_str.parse() { Ok(amount) => amount, Err(_) => return "ERROR Invalid bet amount".to_string(), }; if bet_amount <= 0 { return "ERROR Invalid bet amount".to_string(); } let Some(game) = self.registry.get_game(game_type) else { return format!("ERROR Unknown game type: {}", game_type); }; let Some(balance) = database::get_balance(&self.conn, user) else { return "ERROR Could not retrieve balance".to_string(); }; if balance < 0 { return "ERROR Invalid balance".to_string(); } if let Err(error) = game.validate_bet(bet_amount as u64, balance as u64) { return game_error(error); } let game_state = game.prepare_state(user, bet_amount as u64); let params: Vec<&str> = payload.into_iter().collect(); let new_state = match game.process_action(&game_state, "spin", ¶ms) { Ok(state) => state, Err(error) => return game_error(error), }; let result = match game.calculate_result(&new_state) { Ok(result) => result, Err(error) => return game_error(error), }; // The session is the audit guard for the deduction and is created // before the balance changes. The durable audit row is `plays`. let session_id = self.sessions .create_session(user, game_type.to_string(), new_state.clone()); let winnings = match i64::try_from(result.winnings) { Ok(winnings) => winnings, Err(_) => { let _ = self.sessions.remove_session(&session_id); return "ERROR Winnings out of range".to_string(); } }; let details = serde_json::to_string(&result.result_data).unwrap_or_else(|_| "{}".to_string()); match database::apply_play( &mut self.conn, &session_id, user, game_type, bet_amount, winnings, &details, ) { Ok(new_balance) => { let _ = self.sessions.remove_session(&session_id); if game_type == "roulette" { let number = result .result_data .get("number") .cloned() .unwrap_or_default(); let color = result .result_data .get("color") .cloned() .unwrap_or_default(); let bets = result .result_data .get("bets") .cloned() .unwrap_or_else(|| "{}".to_string()); format!( "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}" ) } else { let reels = result .result_data .get("reels") .cloned() .unwrap_or_default(); let win_type = result .result_data .get("win_type") .cloned() .unwrap_or_default(); let win_lines = result .result_data .get("win_lines") .cloned() .unwrap_or_else(|| "-".to_string()); let win_cells = result .result_data .get("win_cells") .cloned() .unwrap_or_else(|| "-".to_string()); format!( "GAME_RESULT {session_id} {winnings} {bet_amount} {new_balance} {reels} {win_type} {win_lines} {win_cells}" ) } } Err(error) => { let _ = self.sessions.remove_session(&session_id); eprintln!("Balance transaction failed for {user}: {error}"); "ERROR Failed to update balance".to_string() } } } 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); } } } pub fn balance(&self, username: &str) -> Option { database::get_balance(&self.conn, username) } pub fn play_count(&self, username: &str) -> i64 { database::count_plays(&self.conn, username) } pub fn session_count(&self) -> usize { self.sessions.len() } } fn game_error(error: GameError) -> String { match error { GameError::InsufficientBalance => "ERROR Insufficient balance".to_string(), GameError::InvalidBet => "ERROR Invalid bet amount".to_string(), GameError::InvalidAction(message) => format!("ERROR {message}"), GameError::GameOver => "ERROR Game over".to_string(), GameError::SessionNotFound => "ERROR Session not found".to_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()) ) }