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/games/blackjack.rs | 274 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 src/server/games/blackjack.rs (limited to 'src/server/games/blackjack.rs') 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 @@ +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 cancel(&mut self) { + self.phase = "done".to_string(); + self.result = "cancelled".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(), + )) + } +} -- cgit v1.2.3