//! Shared blackjack rules and wire state. use serde::{Deserialize, Serialize}; pub const DECKS: usize = 6; pub const SUITS: [char; 4] = ['C', 'D', 'H', 'S']; pub const RANKS: [&str; 13] = [ "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", ]; /// A fresh, unshuffled six-deck shoe. pub fn fresh_shoe() -> Vec { let mut shoe = Vec::with_capacity(DECKS * 52); for _ in 0..DECKS { for suit in SUITS { for rank in RANKS { shoe.push(format!("{rank}{suit}")); } } } shoe } pub fn card_value(card: &str) -> u8 { let rank = &card[..card.len().saturating_sub(1)]; match rank { "A" => 11, "10" | "J" | "Q" | "K" => 10, _ => rank.parse().unwrap_or(0), } } /// Best total: aces count as 11 until that would bust the hand. pub fn hand_total(cards: &[String]) -> u8 { let mut total = 0u16; let mut aces = 0u16; for card in cards { if card_value(card) == 11 { aces += 1; total += 1; } else { total += card_value(card) as u16; } } for _ in 0..aces { if total + 10 <= 21 { total += 10; } } total.min(255) as u8 } pub fn is_blackjack(cards: &[String]) -> bool { cards.len() == 2 && hand_total(cards) == 21 } pub fn is_red_suit(card: &str) -> bool { matches!(card.chars().last(), Some('D') | Some('H')) } pub fn card_label(card: &str) -> String { let Some(suit) = card.chars().last() else { return card.to_string(); }; let rank = &card[..card.len() - 1]; let suit = match suit { 'C' => '♣', 'D' => '♦', 'H' => '♥', 'S' => '♠', other => other, }; format!("{rank}{suit}") } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BlackjackState { pub session_id: String, pub phase: String, pub player: Vec, pub dealer: Vec, pub player_total: u8, pub dealer_total: u8, pub bet: u64, pub base_bet: u64, pub balance: i64, pub actions: Vec, pub result: String, pub winnings: u64, }