use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; use crate::blackjack::{card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState}; use rand::seq::SliceRandom; use rand::thread_rng; use serde::{Deserialize, Serialize}; use std::collections::HashMap; pub struct BlackjackGame { fixed_deck: Option>, } impl BlackjackGame { pub fn new() -> Self { Self { fixed_deck: None } } pub fn with_deck(deck: Vec) -> Self { Self { fixed_deck: Some(deck), } } fn shoe(&self) -> Vec { if let Some(deck) = &self.fixed_deck { return deck.clone(); } let mut shoe = fresh_shoe(); shoe.shuffle(&mut thread_rng()); shoe } pub fn deal(&self, bet: u64) -> BlackjackHand { let mut hand = BlackjackHand::new(self.shoe(), bet); if is_blackjack(&hand.player) || is_blackjack(&hand.dealer) { hand.settle(); } hand } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlackjackHand { pub shoe: Vec, pub player: Vec, pub dealer: Vec, pub phase: String, pub base_bet: u64, pub bet: u64, pub result: String, pub winnings: u64, } impl BlackjackHand { fn new(shoe: Vec, bet: u64) -> Self { let mut hand = Self { shoe, player: Vec::new(), dealer: Vec::new(), phase: "player".to_string(), base_bet: bet, bet, result: "playing".to_string(), winnings: 0, }; let card = hand.draw(); hand.player.push(card); let card = hand.draw(); hand.dealer.push(card); let card = hand.draw(); hand.player.push(card); let card = hand.draw(); hand.dealer.push(card); hand } fn draw(&mut self) -> String { self.shoe.remove(0) } pub fn player_total(&self) -> u8 { hand_total(&self.player) } pub fn dealer_total(&self) -> u8 { hand_total(&self.dealer) } pub fn is_done(&self) -> bool { self.phase == "done" } pub fn hit(&mut self) -> Result<(), String> { if self.is_done() { return Err("Hand is over".to_string()); } let card = self.draw(); self.player.push(card); let total = self.player_total(); if total > 21 { self.settle(); } else if total == 21 { self.dealer_play(); self.settle(); } Ok(()) } pub fn stand(&mut self) { if self.is_done() { return; } self.dealer_play(); self.settle(); } /// Caller must debit the extra `base_bet` before calling this. pub fn double(&mut self) -> Result<(), String> { if self.is_done() { return Err("Hand is over".to_string()); } if self.player.len() != 2 { return Err("Double is only allowed on the first two cards".to_string()); } self.bet = self.bet.saturating_add(self.base_bet); let card = self.draw(); self.player.push(card); self.dealer_play(); self.settle(); Ok(()) } pub fn forfeit(&mut self) { self.phase = "done".to_string(); self.result = "forfeit".to_string(); self.winnings = 0; } fn dealer_play(&mut self) { while self.dealer_total() < 17 { let card = self.draw(); self.dealer.push(card); } } fn settle(&mut self) { if self.is_done() { return; } self.phase = "done".to_string(); let player = self.player_total(); let dealer = self.dealer_total(); let player_blackjack = is_blackjack(&self.player); let dealer_blackjack = is_blackjack(&self.dealer); self.result = if player > 21 { "bust".to_string() } else if player_blackjack && !dealer_blackjack { "blackjack".to_string() } else if dealer_blackjack && !player_blackjack { "lose".to_string() } else if dealer > 21 { "win".to_string() } else if player == dealer { "push".to_string() } else if player > dealer { "win".to_string() } else { "lose".to_string() }; self.winnings = match self.result.as_str() { "push" => self.bet, "blackjack" => self.bet + (self.bet * 3) / 2, "win" => self.bet * 2, _ => 0, }; } pub fn state(&self, session_id: &str, balance: i64) -> BlackjackState { let player_total = self.player_total(); let (dealer, dealer_total, actions) = if self.is_done() { (self.dealer.clone(), self.dealer_total(), Vec::new()) } else { let visible = self .dealer .first() .map(|card| card_value(card)) .unwrap_or(0); let mut actions = vec!["hit".to_string(), "stand".to_string()]; if self.player.len() == 2 { actions.push("double".to_string()); } ( vec![ self.dealer.first().cloned().unwrap_or_default(), "?".to_string(), ], visible, actions, ) }; BlackjackState { session_id: session_id.to_string(), phase: self.phase.clone(), player: self.player.clone(), dealer, player_total, dealer_total, bet: self.bet, base_bet: self.base_bet, balance, actions, result: self.result.clone(), winnings: self.winnings, } } pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap_or_default() } pub fn from_json(raw: &str) -> Result { serde_json::from_str(raw) } } impl Game for BlackjackGame { fn game_type(&self) -> &'static str { "blackjack" } fn get_name(&self) -> &'static str { "Blackjack" } fn get_description(&self) -> &'static str { "Blackjack: hit, stand or double. Dealer stands on all 17s." } fn prepare_state(&self, _username: &str, bet_amount: u64) -> GameState { let mut data = HashMap::new(); data.insert("status".to_string(), "ready".to_string()); GameState { session_id: String::new(), game_type: "blackjack".to_string(), bet_amount, phase: GamePhase::WaitingToStart, data, } } fn process_action( &self, _state: &GameState, _action: &str, _params: &[&str], ) -> Result { Err(GameError::InvalidAction( "Blackjack uses BLACKJACK_ACTION".to_string(), )) } fn calculate_result(&self, _state: &GameState) -> Result { Err(GameError::InternalError( "Blackjack settles through BLACKJACK_ACTION".to_string(), )) } }