problemmediumoodobject-oriented-design-for-blackjack-deck-of-cardsobject oriented design for blackjack deck of cardsobjectorienteddesignforblackjackdeckofcards

OOD - Blackjack and a Deck of Cards

MediumUpdated: Jan 1, 2026

Problem Statement

Design a complete casino-style Blackjack game system that supports multiple players at a table, proper card dealing mechanics, comprehensive betting options, and authentic game rules. The system must handle the full complexity of professional Blackjack including split hands, double down, insurance bets, surrender options, and dealer rules.

Key requirements include:

  • Deck Management: Support single-deck and multi-deck shoes (1-8 decks) with proper shuffling, card penetration tracking, and automatic reshuffling
  • Player Actions: Hit, stand, double down, split pairs, surrender (early/late), and insurance betting
  • Betting System: Ante bets, side bets (insurance, perfect pairs, 21+3), minimum/maximum table limits, chip denominations
  • Hand Valuation: Correct soft/hard total calculation with Ace handling (1 or 11), blackjack detection (natural 21)
  • Dealer Rules: Automatic dealer play (hit on 16 or less, stand on 17+), configurable S17/H17 rules
  • Payout System: 3:2 for blackjack, 1:1 for regular wins, 2:1 for insurance, push on ties
  • Game Flow: Deal initial cards, offer insurance, player decisions, dealer reveal and play, resolution and payouts
  • Multi-player Support: Multiple players at one table with independent hands and bets
  • Split Hand Management: Handle multiple split hands per player with separate bets and actions
  • Statistics Tracking: Win/loss ratios, bankroll management, session statistics

Requirements Analysis

Functional Requirements

Core Game Operations:

  1. Deck and Shoe Management

    • Create standard 52-card decks with 4 suits and 13 ranks
    • Combine multiple decks into a shoe (typically 6-8 decks)
    • Shuffle using cryptographically secure random algorithm
    • Track card penetration (percentage of shoe dealt)
    • Automatically reshuffle when penetration threshold reached
    • Support cut card placement
  2. Hand Management

    • Create hands from dealt cards
    • Calculate hand values with proper Ace soft/hard logic
    • Detect blackjack (Ace + 10-value card on initial deal)
    • Determine if hand is bust (over 21)
    • Support split hands with independent tracking
    • Validate split eligibility (same rank cards)
  3. Betting Operations

    • Set table minimum and maximum limits
    • Place ante bet before deal
    • Offer insurance when dealer shows Ace
    • Handle split bet (equal to original ante)
    • Handle double down bet (equal to original ante)
    • Support side bets (optional)
    • Validate bet amounts within limits
  4. Player Actions

    • Hit: Request additional card
    • Stand: Keep current hand
    • Double Down: Double bet, receive exactly one more card
    • Split: Split pair into two hands with separate bets
    • Surrender: Forfeit hand for half bet back (early/late)
    • Insurance: Side bet on dealer blackjack (max half of ante)
  5. Dealer Logic

    • Reveal hole card after all players finished
    • Automatically hit until hand value >= 17
    • Support H17 (hit on soft 17) or S17 (stand on soft 17) rules
    • No decision-making required (purely rule-based)
  6. Payout Resolution

    • Blackjack: Pay 3:2 (or 6:5 in some casinos)
    • Regular win: Pay 1:1
    • Insurance win: Pay 2:1
    • Push (tie): Return original bet
    • Loss: Collect bet
    • Surrender: Return half of bet
  7. Game Flow Control

    • Initialize table with dealer and player seats
    • Collect bets from all players
    • Deal two cards to each player and dealer (one face down)
    • Check dealer blackjack if showing Ace or 10
    • Offer insurance when dealer shows Ace
    • Process each player's hand in turn
    • Dealer plays according to rules
    • Resolve all bets and payouts
    • Clear table for next round

Non-Functional Requirements

  1. Fairness: Cryptographically secure shuffling, no card counting advantages through predictable patterns
  2. Performance: Handle 100+ rounds per hour with 7 players, instant hand evaluation
  3. Auditability: Complete game logs for regulatory compliance, all decisions traceable
  4. Configurability: Adjustable rules (H17/S17, surrender options, blackjack payout ratios)
  5. Testability: Deterministic mode for testing with seeded random number generation

Use Case Diagram

graph TB
    subgraph Actors
        Player[šŸ‘¤ Player]
        Dealer[šŸŽ© Dealer]
        Pit[šŸ‘” Pit Boss]
        System[šŸ–„ļø System]
    end

    subgraph "Player Use Cases"
        UC1[šŸ’° Place Bet]
        UC2[šŸƒ Request Hit]
        UC3[āœ‹ Stand on Hand]
        UC4[ā¬†ļø Double Down]
        UC5[āœ‚ļø Split Pairs]
        UC6[šŸ³ļø Surrender Hand]
        UC7[šŸ›”ļø Take Insurance]
        UC8[šŸ’ø Collect Winnings]
        UC9[šŸ“Š View Statistics]
    end

    subgraph "Dealer Use Cases"
        UC10[šŸŽ“ Shuffle Deck]
        UC11[šŸ“¤ Deal Cards]
        UC12[šŸŽÆ Reveal Hole Card]
        UC13[šŸ¤– Play Dealer Hand]
        UC14[šŸ’µ Pay Winning Bets]
        UC15[šŸ“„ Collect Losing Bets]
    end

    subgraph "Pit Boss Use Cases"
        UC16[āš™ļø Configure Table Rules]
        UC17[šŸ’² Set Table Limits]
        UC18[šŸ“‹ Generate Reports]
        UC19[🚫 Close Table]
    end

    subgraph "System Use Cases"
        UC20[šŸ”¢ Calculate Hand Values]
        UC21[āœ… Validate Actions]
        UC22[šŸ”„ Trigger Reshuffle]
        UC23[šŸ“ Log Game Events]
    end

    Player --> UC1
    Player --> UC2
    Player --> UC3
    Player --> UC4
    Player --> UC5
    Player --> UC6
    Player --> UC7
    Player --> UC8
    Player --> UC9

    Dealer --> UC10
    Dealer --> UC11
    Dealer --> UC12
    Dealer --> UC13
    Dealer --> UC14
    Dealer --> UC15

    Pit --> UC16
    Pit --> UC17
    Pit --> UC18
    Pit --> UC19

    System --> UC20
    System --> UC21
    System --> UC22
    System --> UC23

    style UC1 fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style UC2 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style UC3 fill:#e1f5ff,stroke:#01579b,stroke-width:2px
    style UC4 fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
    style UC5 fill:#ffe0b2,stroke:#e65100,stroke-width:2px
    style UC6 fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style UC7 fill:#b2dfdb,stroke:#00695c,stroke-width:2px
    style UC8 fill:#c5e1a5,stroke:#558b2f,stroke-width:2px
    style UC9 fill:#d1c4e9,stroke:#4527a0,stroke-width:2px
    style UC10 fill:#b2ebf2,stroke:#006064,stroke-width:2px
    style UC11 fill:#ffccbc,stroke:#bf360c,stroke-width:2px
    style UC12 fill:#ce93d8,stroke:#6a1b9a,stroke-width:2px
    style UC13 fill:#ffe082,stroke:#f9a825,stroke-width:2px
    style UC14 fill:#a5d6a7,stroke:#388e3c,stroke-width:2px
    style UC15 fill:#ef9a9a,stroke:#c62828,stroke-width:2px
    style UC16 fill:#90caf9,stroke:#1565c0,stroke-width:2px
    style UC17 fill:#ffab91,stroke:#d84315,stroke-width:2px
    style UC18 fill:#b39ddb,stroke:#5e35b1,stroke-width:2px
    style UC19 fill:#f48fb1,stroke:#ad1457,stroke-width:2px
    style UC20 fill:#80deea,stroke:#00695c,stroke-width:2px
    style UC21 fill:#c5cae9,stroke:#3949ab,stroke-width:2px
    style UC22 fill:#bcaaa4,stroke:#5d4037,stroke-width:2px
    style UC23 fill:#e1bee7,stroke:#8e24aa,stroke-width:2px

Class Diagram

The design demonstrates several key patterns:

  • State Pattern: HandState for different hand states (Active, Standing, Busted, Blackjack)
  • Strategy Pattern: DealerStrategy for different dealer rules (H17, S17)
  • Observer Pattern: GameEventListener for tracking game events
  • Command Pattern: PlayerAction for different player moves
  • Chain of Responsibility: PayoutCalculator for determining winnings
classDiagram
    class Card {
        <<value object>>
        -Suit suit
        -Rank rank
        +getValue() int
        +isAce() boolean
        +isTenValue() boolean
    }

    class Suit {
        <<enumeration>>
        HEARTS
        DIAMONDS
        CLUBS
        SPADES
    }

    class Rank {
        <<enumeration>>
        ACE
        TWO
        THREE
        FOUR
        FIVE
        SIX
        SEVEN
        EIGHT
        NINE
        TEN
        JACK
        QUEEN
        KING
        +getValue() int
    }

    class Deck {
        -List~Card~ cards
        +shuffle()
        +deal() Card
        +cardsRemaining() int
        +reset()
    }

    class Shoe {
        -List~Deck~ decks
        -List~Card~ cards
        -int penetration
        -boolean needsShuffle
        +addDeck(deck)
        +shuffle()
        +deal() Card
        +shouldReshuffle() boolean
        +getPenetrationPercent() double
    }

    class Hand {
        -List~Card~ cards
        -HandState state
        -boolean isSplit
        -boolean isDoubled
        +addCard(card)
        +getValues() List~int~
        +getBestValue() int
        +isSoft() boolean
        +isBlackjack() boolean
        +isBusted() boolean
        +canSplit() boolean
        +canDoubleDown() boolean
    }

    class HandState {
        <<interface>>
        +canHit() boolean
        +canStand() boolean
        +canDouble() boolean
        +canSplit() boolean
    }

    class ActiveState {
        +canHit() boolean
        +canStand() boolean
        +canDouble() boolean
        +canSplit() boolean
    }

    class StandingState {
        +canHit() boolean
        +canStand() boolean
        +canDouble() boolean
        +canSplit() boolean
    }

    class BustedState {
        +canHit() boolean
        +canStand() boolean
        +canDouble() boolean
        +canSplit() boolean
    }

    class BlackjackState {
        +canHit() boolean
        +canStand() boolean
        +canDouble() boolean
        +canSplit() boolean
    }

    class Bet {
        -Money amount
        -BetType type
        -BetStatus status
        -Money payout
        +place(amount)
        +resolve(outcome)
        +getPayout() Money
    }

    class BetType {
        <<enumeration>>
        ANTE
        INSURANCE
        SPLIT
        DOUBLE
        SIDE_BET
    }

    class BetStatus {
        <<enumeration>>
        PENDING
        WON
        LOST
        PUSH
        SURRENDERED
    }

    class Money {
        <<value object>>
        -double amount
        +add(other) Money
        +multiply(factor) Money
        +greaterThan(other) boolean
    }

    class Player {
        -String playerId
        -String name
        -Money bankroll
        -List~Hand~ hands
        -List~Bet~ bets
        -PlayerStatistics stats
        +placeBet(amount) Bet
        +addHand(hand)
        +canAffordBet(amount) boolean
        +updateBankroll(amount)
        +getActiveHands() List~Hand~
    }

    class Dealer {
        -Hand hand
        -DealerStrategy strategy
        +shouldHit() boolean
        +playHand(shoe)
        +revealHoleCard()
    }

    class DealerStrategy {
        <<interface>>
        +shouldHit(handValue, isSoft) boolean
    }

    class Stand17Strategy {
        +shouldHit(handValue, isSoft) boolean
    }

    class Hit17Strategy {
        +shouldHit(handValue, isSoft) boolean
    }

    class PlayerAction {
        <<interface>>
        +execute(hand, shoe) ActionResult
        +canExecute(hand) boolean
    }

    class HitAction {
        +execute(hand, shoe) ActionResult
        +canExecute(hand) boolean
    }

    class StandAction {
        +execute(hand, shoe) ActionResult
        +canExecute(hand) boolean
    }

    class DoubleDownAction {
        +execute(hand, shoe) ActionResult
        +canExecute(hand) boolean
    }

    class SplitAction {
        +execute(hand, shoe) ActionResult
        +canExecute(hand) boolean
    }

    class SurrenderAction {
        +execute(hand, shoe) ActionResult
        +canExecute(hand) boolean
    }

    class PayoutCalculator {
        <<interface>>
        +calculate(hand, dealerHand, bet) Money
        +getNextCalculator() PayoutCalculator
    }

    class BlackjackPayoutCalculator {
        -PayoutCalculator next
        -double blackjackRatio
        +calculate(hand, dealerHand, bet) Money
        +getNextCalculator() PayoutCalculator
    }

    class WinPayoutCalculator {
        -PayoutCalculator next
        +calculate(hand, dealerHand, bet) Money
        +getNextCalculator() PayoutCalculator
    }

    class PushPayoutCalculator {
        -PayoutCalculator next
        +calculate(hand, dealerHand, bet) Money
        +getNextCalculator() PayoutCalculator
    }

    class LossPayoutCalculator {
        +calculate(hand, dealerHand, bet) Money
        +getNextCalculator() PayoutCalculator
    }

    class BlackjackTable {
        -String tableId
        -TableRules rules
        -Shoe shoe
        -Dealer dealer
        -List~Player~ players
        -GameState state
        -int currentPlayerIndex
        +addPlayer(player)
        +removePlayer(playerId)
        +startRound()
        +collectBets()
        +dealInitialCards()
        +processPlayerTurns()
        +processDealerTurn()
        +resolveRound()
    }

    class TableRules {
        -Money minimumBet
        -Money maximumBet
        -int numberOfDecks
        -boolean hitSoft17
        -boolean surrenderAllowed
        -boolean doubleAfterSplitAllowed
        -boolean resplitAcesAllowed
        -double blackjackPayout
        +validate(bet) boolean
        +allowsSurrender() boolean
    }

    class GameState {
        <<enumeration>>
        WAITING_FOR_BETS
        DEALING
        INSURANCE_OFFERED
        PLAYER_TURNS
        DEALER_TURN
        RESOLVING
        ROUND_COMPLETE
    }

    class GameEventListener {
        <<interface>>
        +onCardDealt(player, card)
        +onPlayerAction(player, action)
        +onHandResolved(player, hand, outcome)
        +onRoundComplete()
    }

    class GameLogger {
        +onCardDealt(player, card)
        +onPlayerAction(player, action)
        +onHandResolved(player, hand, outcome)
        +onRoundComplete()
    }

    class PlayerStatistics {
        -int handsPlayed
        -int handsWon
        -int handsLost
        -int handsPushed
        -int blackjacks
        -Money totalWagered
        -Money totalWon
        +recordHand(outcome, wager, payout)
        +getWinRate() double
        +getNetProfit() Money
    }

    Card --> Suit
    Card --> Rank

    Deck "1" *-- "52" Card
    Shoe "1" *-- "many" Deck
    Shoe --> Card

    Hand "1" *-- "many" Card
    Hand --> HandState

    HandState <|.. ActiveState
    HandState <|.. StandingState
    HandState <|.. BustedState
    HandState <|.. BlackjackState

    Bet --> Money
    Bet --> BetType
    Bet --> BetStatus

    Player "1" *-- "many" Hand
    Player "1" *-- "many" Bet
    Player --> Money
    Player --> PlayerStatistics

    Dealer "1" *-- "1" Hand
    Dealer --> DealerStrategy

    DealerStrategy <|.. Stand17Strategy
    DealerStrategy <|.. Hit17Strategy

    PlayerAction <|.. HitAction
    PlayerAction <|.. StandAction
    PlayerAction <|.. DoubleDownAction
    PlayerAction <|.. SplitAction
    PlayerAction <|.. SurrenderAction

    PayoutCalculator <|.. BlackjackPayoutCalculator
    PayoutCalculator <|.. WinPayoutCalculator
    PayoutCalculator <|.. PushPayoutCalculator
    PayoutCalculator <|.. LossPayoutCalculator

    BlackjackTable "1" --> "1" Shoe
    BlackjackTable "1" --> "1" Dealer
    BlackjackTable "1" --> "many" Player
    BlackjackTable --> TableRules
    BlackjackTable --> GameState
    BlackjackTable --> GameEventListener

    GameEventListener <|.. GameLogger

    TableRules --> Money

Activity Diagrams

1. Complete Blackjack Round Flow

flowchart TD
    Start([šŸŽ² New Round Starts]) --> ResetTable[šŸ”„ Clear Previous Round<br/>Reset Table State]

    ResetTable --> CheckShuffle{Shoe Needs<br/>Reshuffle?}
    CheckShuffle -->|Yes| Shuffle[šŸ”€ Shuffle All Decks<br/>Reset Penetration Counter]
    CheckShuffle -->|No| CollectBets[šŸ’° Collect Bets from Players]
    Shuffle --> CollectBets

    CollectBets --> ValidateBets{All Bets<br/>Valid?}
    ValidateBets -->|No| InvalidBet[āŒ Reject Invalid Bets<br/>Request Re-entry]
    InvalidBet --> CollectBets

    ValidateBets -->|Yes| DealInitial[šŸŽ“ Deal Two Cards to Each Player<br/>Deal Two Cards to Dealer]
    DealInitial --> SetDealerHole[šŸ”’ Set Dealer's Second Card<br/>Face Down (Hole Card)]

    SetDealerHole --> CheckDealerAce{Dealer Shows<br/>Ace?}
    CheckDealerAce -->|Yes| OfferInsurance[šŸ›”ļø Offer Insurance Bet<br/>to All Players]
    OfferInsurance --> CollectInsurance[šŸ’µ Collect Insurance Bets<br/>(Max: Half of Ante)]
    CollectInsurance --> CheckDealerBJ1{Dealer Has<br/>Blackjack?}

    CheckDealerAce -->|No| CheckDealer10{Dealer Shows<br/>10-Value?}
    CheckDealer10 -->|Yes| CheckDealerBJ2{Dealer Has<br/>Blackjack?}
    CheckDealer10 -->|No| ProcessPlayers

    CheckDealerBJ1 -->|Yes| RevealBJ[šŸŽÆ Reveal Dealer Blackjack]
    CheckDealerBJ2 -->|Yes| RevealBJ

    RevealBJ --> PayInsurance[šŸ’° Pay Insurance Bets 2:1]
    PayInsurance --> CheckPlayerBJ{Any Player<br/>Blackjacks?}
    CheckPlayerBJ -->|Yes| PushBJ[šŸ¤ Push on Player Blackjacks]
    CheckPlayerBJ -->|No| CollectAnte[šŸ“„ Collect All Ante Bets]
    PushBJ --> EndRound
    CollectAnte --> EndRound([šŸ Round Complete])

    CheckDealerBJ1 -->|No| LoseInsurance[šŸ“„ Collect Insurance Bets]
    CheckDealerBJ2 -->|No| LoseInsurance
    LoseInsurance --> ProcessPlayers[šŸ‘„ Process Each Player in Turn]

    ProcessPlayers --> SelectPlayer[šŸ‘¤ Select Next Player]
    SelectPlayer --> SelectHand[āœ‹ Select Next Active Hand]

    SelectHand --> CheckPlayerBJ2{Player Has<br/>Blackjack?}
    CheckPlayerBJ2 -->|Yes| MarkBJ[šŸŽŠ Mark Hand as Blackjack<br/>No Further Action]
    MarkBJ --> NextHand{More Hands<br/>for Player?}

    CheckPlayerBJ2 -->|No| OfferActions[šŸŽÆ Offer Available Actions]
    OfferActions --> PlayerChoice{Player<br/>Action?}

    PlayerChoice -->|Hit| DealCard[šŸƒ Deal One Card]
    DealCard --> UpdateValue[šŸ”¢ Recalculate Hand Value]
    UpdateValue --> CheckBust{Hand<br/>Busted?}
    CheckBust -->|Yes| MarkBust[šŸ’„ Mark Hand as Busted]
    CheckBust -->|No| OfferActions
    MarkBust --> NextHand

    PlayerChoice -->|Stand| MarkStand[āœ‹ Mark Hand as Standing]
    MarkStand --> NextHand

    PlayerChoice -->|Double| ValidateDouble{Can<br/>Double?}
    ValidateDouble -->|No| ShowError[āŒ Show Error<br/>Invalid Action]
    ShowError --> OfferActions
    ValidateDouble -->|Yes| PlaceDouble[šŸ’° Place Double Bet<br/>Equal to Ante]
    PlaceDouble --> DealOneCard[šŸŽ“ Deal Exactly One Card]
    DealOneCard --> CheckBustDouble{Hand<br/>Busted?}
    CheckBustDouble -->|Yes| MarkBust
    CheckBustDouble -->|No| MarkStandDouble[āœ… Auto-Stand After Double]
    MarkStandDouble --> NextHand

    PlayerChoice -->|Split| ValidateSplit{Can Split?<br/>(Same Rank)}
    ValidateSplit -->|No| ShowError
    ValidateSplit -->|Yes| PlaceSplit[šŸ’° Place Split Bet<br/>Equal to Ante]
    PlaceSplit --> CreateHands[āœ‚ļø Create Two Hands<br/>from Split Cards]
    CreateHands --> DealToSplits[šŸŽ“ Deal One Card<br/>to Each New Hand]
    DealToSplits --> OfferActions

    PlayerChoice -->|Surrender| ValidateSurrender{Surrender<br/>Allowed?}
    ValidateSurrender -->|No| ShowError
    ValidateSurrender -->|Yes| ReturnHalf[šŸ’µ Return Half of Bet]
    ReturnHalf --> MarkSurrendered[šŸ³ļø Mark Hand as Surrendered]
    MarkSurrendered --> NextHand

    NextHand -->|Yes| SelectHand
    NextHand -->|No| NextPlayer{More<br/>Players?}
    NextPlayer -->|Yes| SelectPlayer
    NextPlayer -->|No| DealerTurn[šŸŽ© Dealer's Turn]

    DealerTurn --> RevealHole[šŸ”“ Reveal Dealer Hole Card]
    RevealHole --> CalcDealerValue[šŸ”¢ Calculate Dealer Hand Value]
    CalcDealerValue --> DealerRule{Hand Value<br/>< 17?}

    DealerRule -->|Yes| DealerHit[šŸƒ Dealer Hits<br/>(Auto-Play)]
    DealerHit --> CheckDealerBust{Dealer<br/>Busted?}
    CheckDealerBust -->|Yes| DealerBusted[šŸ’„ Dealer Busted<br/>All Active Players Win]
    DealerBusted --> ResolveRound
    CheckDealerBust -->|No| CalcDealerValue

    DealerRule -->|No| DealerStand[āœ‹ Dealer Stands]
    DealerStand --> ResolveRound[šŸŽÆ Resolve All Hands]

    ResolveRound --> CompareHands[āš–ļø Compare Each Player Hand<br/>to Dealer Hand]
    CompareHands --> CalcPayouts[šŸ’µ Calculate Payouts<br/>Using Chain of Responsibility]

    CalcPayouts --> PayWinners[šŸ’° Pay Winning Hands]
    PayWinners --> CollectLosers[šŸ“„ Collect Losing Bets]
    CollectLosers --> ReturnPushes[šŸ¤ Return Push Bets]

    ReturnPushes --> UpdateStats[šŸ“Š Update Player Statistics<br/>Record Win/Loss/Push]
    UpdateStats --> LogRound[šŸ“ Log Round Results<br/>for Audit Trail]

    LogRound --> EndRound

    style Start fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
    style ResetTable fill:#b2dfdb,stroke:#00695c,stroke-width:2px
    style CheckShuffle fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style Shuffle fill:#ce93d8,stroke:#6a1b9a,stroke-width:2px
    style CollectBets fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style ValidateBets fill:#ffe082,stroke:#f9a825,stroke-width:2px
    style DealInitial fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
    style SetDealerHole fill:#e1bee7,stroke:#8e24aa,stroke-width:2px
    style CheckDealerAce fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style OfferInsurance fill:#b3e5fc,stroke:#0277bd,stroke-width:2px
    style CheckDealerBJ1 fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style CheckDealer10 fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style CheckDealerBJ2 fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style RevealBJ fill:#f48fb1,stroke:#ad1457,stroke-width:2px
    style PayInsurance fill:#a5d6a7,stroke:#388e3c,stroke-width:2px
    style ProcessPlayers fill:#bbdefb,stroke:#1976d2,stroke-width:2px
    style SelectPlayer fill:#b2ebf2,stroke:#006064,stroke-width:2px
    style SelectHand fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style OfferActions fill:#c5e1a5,stroke:#558b2f,stroke-width:2px
    style PlayerChoice fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style DealCard fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style CheckBust fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style MarkBust fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style MarkStand fill:#b2dfdb,stroke:#00796b,stroke-width:2px
    style PlaceDouble fill:#ffe0b2,stroke:#e65100,stroke-width:2px
    style DealOneCard fill:#dcedc8,stroke:#558b2f,stroke-width:2px
    style PlaceSplit fill:#ffccbc,stroke:#d84315,stroke-width:2px
    style CreateHands fill:#ce93d8,stroke:#6a1b9a,stroke-width:2px
    style ReturnHalf fill:#c5e1a5,stroke:#558b2f,stroke-width:2px
    style DealerTurn fill:#f8bbd0,stroke:#c2185b,stroke-width:2px
    style RevealHole fill:#d1c4e9,stroke:#5e35b1,stroke-width:2px
    style DealerRule fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style DealerHit fill:#ffccbc,stroke:#d84315,stroke-width:2px
    style DealerStand fill:#b2dfdb,stroke:#00796b,stroke-width:2px
    style ResolveRound fill:#c5cae9,stroke:#3949ab,stroke-width:2px
    style CompareHands fill:#bbdefb,stroke:#1565c0,stroke-width:2px
    style CalcPayouts fill:#ffe082,stroke:#f9a825,stroke-width:2px
    style PayWinners fill:#a5d6a7,stroke:#2e7d32,stroke-width:2px
    style CollectLosers fill:#ef9a9a,stroke:#c62828,stroke-width:2px
    style ReturnPushes fill:#80deea,stroke:#006064,stroke-width:2px
    style UpdateStats fill:#d1c4e9,stroke:#5e35b1,stroke-width:2px
    style LogRound fill:#bcaaa4,stroke:#5d4037,stroke-width:2px
    style InvalidBet fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style ShowError fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style EndRound fill:#66bb6a,stroke:#2e7d32,stroke-width:3px

2. Hand Value Calculation with Ace Logic

flowchart TD
    Start([šŸ”¢ Calculate Hand Value]) --> Initialize[šŸ“Š Initialize Total = 0<br/>Initialize Ace Count = 0]

    Initialize --> IterateCards[šŸ”„ Iterate Through All Cards<br/>in Hand]

    IterateCards --> NextCard[šŸƒ Get Next Card]
    NextCard --> CheckAce{Is Card<br/>an Ace?}

    CheckAce -->|Yes| CountAce[āž• Increment Ace Count]
    CountAce --> AddOne[āž• Add 1 to Total<br/>(Ace as 1)]

    CheckAce -->|No| CheckFace{Is Card<br/>Face Card?}
    CheckFace -->|Yes - J,Q,K| AddTen[āž• Add 10 to Total]
    CheckFace -->|No - Number Card| AddValue[āž• Add Card Value to Total]

    AddOne --> MoreCards{More Cards<br/>in Hand?}
    AddTen --> MoreCards
    AddValue --> MoreCards

    MoreCards -->|Yes| NextCard
    MoreCards -->|No| CheckAceOptimize{Ace Count<br/>> 0?}

    CheckAceOptimize -->|No| ReturnHard[šŸ“¤ Return Hard Total<br/>No Aces in Hand]
    ReturnHard --> End([āœ… Value Calculated])

    CheckAceOptimize -->|Yes| TryElevate[šŸŽÆ Try Elevating One Ace<br/>Add 10 to Total (Ace as 11)]

    TryElevate --> CheckSoft{Total + 10<br/>≤ 21?}

    CheckSoft -->|Yes| ReturnSoft[šŸ“¤ Return Soft Total<br/>One Ace Counts as 11]
    CheckSoft -->|No| ReturnMultipleAces[šŸ“¤ Return Hard Total<br/>All Aces Count as 1]

    ReturnSoft --> MarkSoft[šŸ·ļø Mark Hand as Soft<br/>for Display Purposes]
    ReturnMultipleAces --> MarkHard[šŸ·ļø Mark Hand as Hard]

    MarkSoft --> End
    MarkHard --> End

    style Start fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
    style Initialize fill:#b2dfdb,stroke:#00695c,stroke-width:2px
    style IterateCards fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style NextCard fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style CheckAce fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style CountAce fill:#bbdefb,stroke:#1976d2,stroke-width:2px
    style AddOne fill:#dcedc8,stroke:#558b2f,stroke-width:2px
    style CheckFace fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style AddTen fill:#ffe0b2,stroke:#e65100,stroke-width:2px
    style AddValue fill:#c5e1a5,stroke:#558b2f,stroke-width:2px
    style MoreCards fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style CheckAceOptimize fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style TryElevate fill:#ce93d8,stroke:#6a1b9a,stroke-width:2px
    style CheckSoft fill:#ffe082,stroke:#f9a825,stroke-width:2px
    style ReturnSoft fill:#a5d6a7,stroke:#2e7d32,stroke-width:2px
    style ReturnMultipleAces fill:#ffccbc,stroke:#d84315,stroke-width:2px
    style ReturnHard fill:#b2ebf2,stroke:#006064,stroke-width:2px
    style MarkSoft fill:#c5cae9,stroke:#3949ab,stroke-width:2px
    style MarkHard fill:#f8bbd0,stroke:#c2185b,stroke-width:2px
    style End fill:#66bb6a,stroke:#2e7d32,stroke-width:3px

3. Payout Calculation Chain of Responsibility

flowchart TD
    Start([šŸ’° Calculate Payout]) --> GetHand[šŸ“‹ Get Player Hand & Bet Info]
    GetHand --> GetDealer[šŸŽ© Get Dealer Final Hand]

    GetDealer --> CheckPlayerBust{Player<br/>Busted?}
    CheckPlayerBust -->|Yes| PlayerLoss[āŒ Player Loses<br/>Payout = $0]
    PlayerLoss --> End([šŸ’µ Return Payout])

    CheckPlayerBust -->|No| CheckSurrender{Player<br/>Surrendered?}
    CheckSurrender -->|Yes| HalfBack[šŸ’° Return Half of Bet<br/>Payout = Bet Ɨ 0.5]
    HalfBack --> End

    CheckSurrender -->|No| CheckDealerBust{Dealer<br/>Busted?}
    CheckDealerBust -->|Yes| PlayerWins[āœ… Player Wins<br/>Payout = Bet Ɨ 1]
    PlayerWins --> End

    CheckDealerBust -->|No| CheckPlayerBJ{Player Has<br/>Blackjack?}

    CheckPlayerBJ -->|Yes| CheckDealerBJ{Dealer Has<br/>Blackjack?}
    CheckDealerBJ -->|Yes| PushBJ[šŸ¤ Push on Both Blackjacks<br/>Payout = Bet Ɨ 0 (returned)]
    PushBJ --> End

    CheckDealerBJ -->|No| BJWin[šŸŽŠ Blackjack Wins!<br/>Payout = Bet Ɨ 1.5<br/>(3:2 ratio)]
    BJWin --> End

    CheckPlayerBJ -->|No| GetPlayerValue[šŸ”¢ Get Best Player Value]
    GetPlayerValue --> GetDealerValue[šŸ”¢ Get Dealer Value]

    GetDealerValue --> Compare{Compare<br/>Values}

    Compare -->|Player > Dealer| RegularWin[āœ… Player Wins<br/>Payout = Bet Ɨ 1]
    RegularWin --> End

    Compare -->|Player == Dealer| Push[šŸ¤ Push (Tie)<br/>Payout = Bet Ɨ 0 (returned)]
    Push --> End

    Compare -->|Player < Dealer| Loss[āŒ Player Loses<br/>Payout = $0]
    Loss --> End

    style Start fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
    style GetHand fill:#b2dfdb,stroke:#00695c,stroke-width:2px
    style GetDealer fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style CheckPlayerBust fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style PlayerLoss fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style CheckSurrender fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style HalfBack fill:#ffe0b2,stroke:#e65100,stroke-width:2px
    style CheckDealerBust fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style PlayerWins fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style CheckPlayerBJ fill:#fff59d,stroke:#fbc02d,stroke-width:2px
    style CheckDealerBJ fill:#ffe082,stroke:#f9a825,stroke-width:2px
    style PushBJ fill:#80deea,stroke:#006064,stroke-width:2px
    style BJWin fill:#a5d6a7,stroke:#388e3c,stroke-width:2px
    style GetPlayerValue fill:#bbdefb,stroke:#1976d2,stroke-width:2px
    style GetDealerValue fill:#ce93d8,stroke:#6a1b9a,stroke-width:2px
    style Compare fill:#fff9c4,stroke:#f57f17,stroke-width:2px
    style RegularWin fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style Push fill:#b2ebf2,stroke:#006064,stroke-width:2px
    style Loss fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style End fill:#66bb6a,stroke:#2e7d32,stroke-width:3px

Implementation

Java Implementation

package blackjack;

import java.util.*;
import java.util.stream.Collectors;

// =============== Enumerations ===============

enum Suit {
    HEARTS("ā™„ļø"), DIAMONDS("ā™¦ļø"), CLUBS("ā™£ļø"), SPADES("ā™ ļø");

    private final String symbol;

    Suit(String symbol) {
        this.symbol = symbol;
    }

    public String getSymbol() {
        return symbol;
    }
}

enum Rank {
    ACE(1, "A"), TWO(2, "2"), THREE(3, "3"), FOUR(4, "4"),
    FIVE(5, "5"), SIX(6, "6"), SEVEN(7, "7"), EIGHT(8, "8"),
    NINE(9, "9"), TEN(10, "10"), JACK(10, "J"),
    QUEEN(10, "Q"), KING(10, "K");

    private final int value;
    private final String symbol;

    Rank(int value, String symbol) {
        this.value = value;
        this.symbol = symbol;
    }

    public int getValue() {
        return value;
    }

    public String getSymbol() {
        return symbol;
    }
}

enum BetType {
    ANTE, INSURANCE, SPLIT, DOUBLE, SIDE_BET
}

enum BetStatus {
    PENDING, WON, LOST, PUSH, SURRENDERED
}

enum GameState {
    WAITING_FOR_BETS, DEALING, INSURANCE_OFFERED,
    PLAYER_TURNS, DEALER_TURN, RESOLVING, ROUND_COMPLETE
}

// =============== Value Objects ===============

class Money {
    private final double amount;

    public Money(double amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        this.amount = amount;
    }

    public Money add(Money other) {
        return new Money(this.amount + other.amount);
    }

    public Money subtract(Money other) {
        return new Money(this.amount - other.amount);
    }

    public Money multiply(double factor) {
        return new Money(this.amount * factor);
    }

    public boolean greaterThan(Money other) {
        return this.amount > other.amount;
    }

    public boolean greaterThanOrEqual(Money other) {
        return this.amount >= other.amount;
    }

    public double getAmount() {
        return amount;
    }

    @Override
    public String toString() {
        return String.format("$%.2f", amount);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Money)) return false;
        Money other = (Money) obj;
        return Double.compare(amount, other.amount) == 0;
    }
}

class Card {
    private final Suit suit;
    private final Rank rank;

    public Card(Suit suit, Rank rank) {
        this.suit = suit;
        this.rank = rank;
    }

    public int getValue() {
        return rank.getValue();
    }

    public boolean isAce() {
        return rank == Rank.ACE;
    }

    public boolean isTenValue() {
        return rank.getValue() == 10;
    }

    public Suit getSuit() {
        return suit;
    }

    public Rank getRank() {
        return rank;
    }

    @Override
    public String toString() {
        return rank.getSymbol() + suit.getSymbol();
    }
}

// =============== Deck and Shoe ===============

class Deck {
    private List<Card> cards;

    public Deck() {
        this.cards = new ArrayList<>();
        initializeDeck();
    }

    private void initializeDeck() {
        for (Suit suit : Suit.values()) {
            for (Rank rank : Rank.values()) {
                cards.add(new Card(suit, rank));
            }
        }
    }

    public void shuffle() {
        Collections.shuffle(cards);
    }

    public Card deal() {
        if (cards.isEmpty()) {
            throw new IllegalStateException("Deck is empty");
        }
        return cards.remove(cards.size() - 1);
    }

    public int cardsRemaining() {
        return cards.size();
    }

    public void reset() {
        cards.clear();
        initializeDeck();
    }

    public List<Card> getCards() {
        return new ArrayList<>(cards);
    }
}

class Shoe {
    private final int numberOfDecks;
    private List<Card> cards;
    private int totalCards;
    private double penetrationThreshold = 0.75; // Reshuffle at 75% penetration

    public Shoe(int numberOfDecks) {
        if (numberOfDecks < 1 || numberOfDecks > 8) {
            throw new IllegalArgumentException("Number of decks must be between 1 and 8");
        }
        this.numberOfDecks = numberOfDecks;
        this.cards = new ArrayList<>();
        initializeShoe();
    }

    private void initializeShoe() {
        cards.clear();
        for (int i = 0; i < numberOfDecks; i++) {
            Deck deck = new Deck();
            cards.addAll(deck.getCards());
        }
        this.totalCards = cards.size();
    }

    public void shuffle() {
        Collections.shuffle(cards);
        System.out.println("šŸ”€ Shoe shuffled with " + numberOfDecks + " decks (" + cards.size() + " cards)");
    }

    public Card deal() {
        if (shouldReshuffle()) {
            initializeShoe();
            shuffle();
        }
        if (cards.isEmpty()) {
            throw new IllegalStateException("Shoe is empty");
        }
        return cards.remove(cards.size() - 1);
    }

    public boolean shouldReshuffle() {
        return getPenetrationPercent() >= penetrationThreshold;
    }

    public double getPenetrationPercent() {
        return 1.0 - ((double) cards.size() / totalCards);
    }

    public int cardsRemaining() {
        return cards.size();
    }
}

// =============== State Pattern for Hand ===============

interface HandState {
    boolean canHit();
    boolean canStand();
    boolean canDouble();
    boolean canSplit();
    String getStateName();
}

class ActiveState implements HandState {
    public boolean canHit() { return true; }
    public boolean canStand() { return true; }
    public boolean canDouble() { return true; }
    public boolean canSplit() { return true; }
    public String getStateName() { return "Active"; }
}

class StandingState implements HandState {
    public boolean canHit() { return false; }
    public boolean canStand() { return false; }
    public boolean canDouble() { return false; }
    public boolean canSplit() { return false; }
    public String getStateName() { return "Standing"; }
}

class BustedState implements HandState {
    public boolean canHit() { return false; }
    public boolean canStand() { return false; }
    public boolean canDouble() { return false; }
    public boolean canSplit() { return false; }
    public String getStateName() { return "Busted"; }
}

class BlackjackState implements HandState {
    public boolean canHit() { return false; }
    public boolean canStand() { return false; }
    public boolean canDouble() { return false; }
    public boolean canSplit() { return false; }
    public String getStateName() { return "Blackjack"; }
}

class SurrenderedState implements HandState {
    public boolean canHit() { return false; }
    public boolean canStand() { return false; }
    public boolean canDouble() { return false; }
    public boolean canSplit() { return false; }
    public String getStateName() { return "Surrendered"; }
}

// =============== Hand ===============

class Hand {
    private List<Card> cards;
    private HandState state;
    private boolean isSplit;
    private boolean isDoubled;

    public Hand() {
        this.cards = new ArrayList<>();
        this.state = new ActiveState();
        this.isSplit = false;
        this.isDoubled = false;
    }

    public void addCard(Card card) {
        cards.add(card);

        // Update state based on hand value
        if (cards.size() == 2 && getBestValue() == 21 && !isSplit) {
            state = new BlackjackState();
        } else if (getBestValue() > 21) {
            state = new BustedState();
        }
    }

    public List<Integer> getValues() {
        int total = 0;
        int aceCount = 0;

        // Calculate base total with all Aces as 1
        for (Card card : cards) {
            if (card.isAce()) {
                aceCount++;
                total += 1;
            } else {
                total += card.getValue();
            }
        }

        List<Integer> values = new ArrayList<>();
        values.add(total); // Hard total

        // Try elevating each Ace to 11
        for (int i = 0; i < aceCount; i++) {
            total += 10; // Change one Ace from 1 to 11
            if (total <= 21) {
                values.add(total);
            }
        }

        return values;
    }

    public int getBestValue() {
        List<Integer> values = getValues();

        // Find highest value <= 21
        int best = values.get(0);
        for (int value : values) {
            if (value <= 21 && value > best) {
                best = value;
            }
        }

        return best;
    }

    public boolean isSoft() {
        List<Integer> values = getValues();
        return values.size() > 1 && values.get(values.size() - 1) <= 21;
    }

    public boolean isBlackjack() {
        return state instanceof BlackjackState;
    }

    public boolean isBusted() {
        return state instanceof BustedState;
    }

    public boolean canSplit() {
        return cards.size() == 2 &&
               cards.get(0).getRank() == cards.get(1).getRank() &&
               state.canSplit();
    }

    public boolean canDoubleDown() {
        return cards.size() == 2 && state.canDouble();
    }

    public void stand() {
        if (state.canStand()) {
            state = new StandingState();
        }
    }

    public void surrender() {
        state = new SurrenderedState();
    }

    public void markAsDoubled() {
        this.isDoubled = true;
    }

    public void markAsSplit() {
        this.isSplit = true;
    }

    // Getters
    public List<Card> getCards() { return new ArrayList<>(cards); }
    public HandState getState() { return state; }
    public boolean isSplitHand() { return isSplit; }
    public boolean isDoubledHand() { return isDoubled; }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < cards.size(); i++) {
            sb.append(cards.get(i));
            if (i < cards.size() - 1) sb.append(" ");
        }
        sb.append("] = ");

        if (isBusted()) {
            sb.append("BUST");
        } else if (isBlackjack()) {
            sb.append("BLACKJACK!");
        } else if (isSoft()) {
            List<Integer> values = getValues();
            sb.append("Soft ").append(values.get(values.size() - 1));
        } else {
            sb.append(getBestValue());
        }

        return sb.toString();
    }
}

// =============== Bet ===============

class Bet {
    private final Money amount;
    private final BetType type;
    private BetStatus status;
    private Money payout;

    public Bet(Money amount, BetType type) {
        this.amount = amount;
        this.type = type;
        this.status = BetStatus.PENDING;
        this.payout = new Money(0);
    }

    public void resolve(BetStatus status, Money payout) {
        this.status = status;
        this.payout = payout;
    }

    public Money getNetResult() {
        if (status == BetStatus.WON) {
            return payout;
        } else if (status == BetStatus.LOST) {
            return amount.multiply(-1);
        } else if (status == BetStatus.PUSH) {
            return new Money(0);
        } else if (status == BetStatus.SURRENDERED) {
            return amount.multiply(-0.5);
        }
        return new Money(0);
    }

    // Getters
    public Money getAmount() { return amount; }
    public BetType getType() { return type; }
    public BetStatus getStatus() { return status; }
    public Money getPayout() { return payout; }
}

// =============== Chain of Responsibility for Payouts ===============

interface PayoutCalculator {
    Money calculate(Hand playerHand, Hand dealerHand, Bet bet);
}

class BlackjackPayoutCalculator implements PayoutCalculator {
    private final PayoutCalculator next;
    private final double blackjackRatio;

    public BlackjackPayoutCalculator(PayoutCalculator next, double blackjackRatio) {
        this.next = next;
        this.blackjackRatio = blackjackRatio;
    }

    @Override
    public Money calculate(Hand playerHand, Hand dealerHand, Bet bet) {
        if (playerHand.isBlackjack() && !dealerHand.isBlackjack()) {
            return bet.getAmount().multiply(blackjackRatio);
        }
        return next.calculate(playerHand, dealerHand, bet);
    }
}

class WinPayoutCalculator implements PayoutCalculator {
    private final PayoutCalculator next;

    public WinPayoutCalculator(PayoutCalculator next) {
        this.next = next;
    }

    @Override
    public Money calculate(Hand playerHand, Hand dealerHand, Bet bet) {
        if (!playerHand.isBusted() &&
            (dealerHand.isBusted() || playerHand.getBestValue() > dealerHand.getBestValue())) {
            return bet.getAmount(); // 1:1 payout
        }
        return next.calculate(playerHand, dealerHand, bet);
    }
}

class PushPayoutCalculator implements PayoutCalculator {
    private final PayoutCalculator next;

    public PushPayoutCalculator(PayoutCalculator next) {
        this.next = next;
    }

    @Override
    public Money calculate(Hand playerHand, Hand dealerHand, Bet bet) {
        if (!playerHand.isBusted() && !dealerHand.isBusted() &&
            playerHand.getBestValue() == dealerHand.getBestValue()) {
            return new Money(0); // Push - bet returned
        }
        return next.calculate(playerHand, dealerHand, bet);
    }
}

class LossPayoutCalculator implements PayoutCalculator {
    @Override
    public Money calculate(Hand playerHand, Hand dealerHand, Bet bet) {
        return new Money(0); // Player loses - no payout
    }
}

// Continue in next part due to length...

Python Implementation (Key Components)

from enum import Enum
from dataclasses import dataclass, field
from typing import List, Optional, Protocol
from abc import ABC, abstractmethod
import random

# =============== Enumerations ===============

class Suit(Enum):
    HEARTS = ("ā™„ļø", "hearts")
    DIAMONDS = ("ā™¦ļø", "diamonds")
    CLUBS = ("ā™£ļø", "clubs")
    SPADES = ("ā™ ļø", "spades")

    def __init__(self, symbol: str, name: str):
        self.symbol = symbol
        self.suit_name = name

class Rank(Enum):
    ACE = (1, "A")
    TWO = (2, "2")
    THREE = (3, "3")
    FOUR = (4, "4")
    FIVE = (5, "5")
    SIX = (6, "6")
    SEVEN = (7, "7")
    EIGHT = (8, "8")
    NINE = (9, "9")
    TEN = (10, "10")
    JACK = (10, "J")
    QUEEN = (10, "Q")
    KING = (10, "K")

    def __init__(self, value: int, symbol: str):
        self.card_value = value
        self.symbol = symbol

class BetType(Enum):
    ANTE = "ante"
    INSURANCE = "insurance"
    SPLIT = "split"
    DOUBLE = "double"
    SIDE_BET = "side_bet"

class BetStatus(Enum):
    PENDING = "pending"
    WON = "won"
    LOST = "lost"
    PUSH = "push"
    SURRENDERED = "surrendered"

class GameState(Enum):
    WAITING_FOR_BETS = "waiting_for_bets"
    DEALING = "dealing"
    INSURANCE_OFFERED = "insurance_offered"
    PLAYER_TURNS = "player_turns"
    DEALER_TURN = "dealer_turn"
    RESOLVING = "resolving"
    ROUND_COMPLETE = "round_complete"

# =============== Value Objects ===============

@dataclass(frozen=True)
class Money:
    amount: float

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Amount cannot be negative")

    def add(self, other: 'Money') -> 'Money':
        return Money(self.amount + other.amount)

    def subtract(self, other: 'Money') -> 'Money':
        return Money(self.amount - other.amount)

    def multiply(self, factor: float) -> 'Money':
        return Money(self.amount * factor)

    def greater_than(self, other: 'Money') -> bool:
        return self.amount > other.amount

    def __str__(self) -> str:
        return f"${self.amount:.2f}"

@dataclass(frozen=True)
class Card:
    suit: Suit
    rank: Rank

    def get_value(self) -> int:
        return self.rank.card_value

    def is_ace(self) -> bool:
        return self.rank == Rank.ACE

    def is_ten_value(self) -> bool:
        return self.rank.card_value == 10

    def __str__(self) -> str:
        return f"{self.rank.symbol}{self.suit.symbol}"

# =============== Deck and Shoe ===============

class Deck:
    def __init__(self):
        self.cards: List[Card] = []
        self._initialize_deck()

    def _initialize_deck(self):
        self.cards = [
            Card(suit, rank)
            for suit in Suit
            for rank in Rank
        ]

    def shuffle(self):
        random.shuffle(self.cards)

    def deal(self) -> Card:
        if not self.cards:
            raise ValueError("Deck is empty")
        return self.cards.pop()

    def cards_remaining(self) -> int:
        return len(self.cards)

    def reset(self):
        self.cards.clear()
        self._initialize_deck()

class Shoe:
    def __init__(self, number_of_decks: int):
        if not 1 <= number_of_decks <= 8:
            raise ValueError("Number of decks must be between 1 and 8")

        self.number_of_decks = number_of_decks
        self.cards: List[Card] = []
        self.total_cards = 0
        self.penetration_threshold = 0.75
        self._initialize_shoe()

    def _initialize_shoe(self):
        self.cards.clear()
        for _ in range(self.number_of_decks):
            deck = Deck()
            self.cards.extend(deck.cards)
        self.total_cards = len(self.cards)

    def shuffle(self):
        random.shuffle(self.cards)
        print(f"šŸ”€ Shoe shuffled with {self.number_of_decks} decks ({len(self.cards)} cards)")

    def deal(self) -> Card:
        if self.should_reshuffle():
            self._initialize_shoe()
            self.shuffle()

        if not self.cards:
            raise ValueError("Shoe is empty")

        return self.cards.pop()

    def should_reshuffle(self) -> bool:
        return self.get_penetration_percent() >= self.penetration_threshold

    def get_penetration_percent(self) -> float:
        return 1.0 - (len(self.cards) / self.total_cards)

    def cards_remaining(self) -> int:
        return len(self.cards)

# =============== State Pattern for Hand ===============

class HandState(ABC):
    @abstractmethod
    def can_hit(self) -> bool:
        pass

    @abstractmethod
    def can_stand(self) -> bool:
        pass

    @abstractmethod
    def can_double(self) -> bool:
        pass

    @abstractmethod
    def can_split(self) -> bool:
        pass

    @abstractmethod
    def get_state_name(self) -> str:
        pass

class ActiveState(HandState):
    def can_hit(self) -> bool: return True
    def can_stand(self) -> bool: return True
    def can_double(self) -> bool: return True
    def can_split(self) -> bool: return True
    def get_state_name(self) -> str: return "Active"

class StandingState(HandState):
    def can_hit(self) -> bool: return False
    def can_stand(self) -> bool: return False
    def can_double(self) -> bool: return False
    def can_split(self) -> bool: return False
    def get_state_name(self) -> str: return "Standing"

class BustedState(HandState):
    def can_hit(self) -> bool: return False
    def can_stand(self) -> bool: return False
    def can_double(self) -> bool: return False
    def can_split(self) -> bool: return False
    def get_state_name(self) -> str: return "Busted"

class BlackjackState(HandState):
    def can_hit(self) -> bool: return False
    def can_stand(self) -> bool: return False
    def can_double(self) -> bool: return False
    def can_split(self) -> bool: return False
    def get_state_name(self) -> str: return "Blackjack"

# =============== Hand ===============

class Hand:
    def __init__(self):
        self.cards: List[Card] = []
        self.state: HandState = ActiveState()
        self.is_split = False
        self.is_doubled = False

    def add_card(self, card: Card):
        self.cards.append(card)

        # Update state based on hand value
        if len(self.cards) == 2 and self.get_best_value() == 21 and not self.is_split:
            self.state = BlackjackState()
        elif self.get_best_value() > 21:
            self.state = BustedState()

    def get_values(self) -> List[int]:
        total = 0
        ace_count = 0

        for card in self.cards:
            if card.is_ace():
                ace_count += 1
                total += 1
            else:
                total += card.get_value()

        values = [total]  # Hard total

        # Try elevating Aces to 11
        for _ in range(ace_count):
            total += 10
            if total <= 21:
                values.append(total)

        return values

    def get_best_value(self) -> int:
        values = self.get_values()

        # Find highest value <= 21
        best = values[0]
        for value in values:
            if value <= 21 and value > best:
                best = value

        return best

    def is_soft(self) -> bool:
        values = self.get_values()
        return len(values) > 1 and values[-1] <= 21

    def is_blackjack(self) -> bool:
        return isinstance(self.state, BlackjackState)

    def is_busted(self) -> bool:
        return isinstance(self.state, BustedState)

    def can_split_hand(self) -> bool:
        return (len(self.cards) == 2 and
                self.cards[0].rank == self.cards[1].rank and
                self.state.can_split())

    def can_double_down(self) -> bool:
        return len(self.cards) == 2 and self.state.can_double()

    def stand(self):
        if self.state.can_stand():
            self.state = StandingState()

    def mark_as_doubled(self):
        self.is_doubled = True

    def mark_as_split(self):
        self.is_split = True

    def __str__(self) -> str:
        cards_str = " ".join(str(card) for card in self.cards)

        if self.is_busted():
            value_str = "BUST"
        elif self.is_blackjack():
            value_str = "BLACKJACK!"
        elif self.is_soft():
            value_str = f"Soft {self.get_values()[-1]}"
        else:
            value_str = str(self.get_best_value())

        return f"[{cards_str}] = {value_str}"

# Continue with remaining classes (Dealer Strategy, Payout Calculator, etc.)

Key Design Decisions

  1. State Pattern for Hand Management: Allows hands to transition through states (Active → Standing/Busted/Blackjack) with proper validation of allowed actions at each state.

  2. Chain of Responsibility for Payouts: Separates payout logic into distinct calculators (Blackjack → Win → Push → Loss) that can be easily modified or extended.

  3. Strategy Pattern for Dealer Rules: Enables different dealer strategies (H17 vs S17) without modifying the Dealer class, supporting various casino rules.

  4. Observer Pattern for Game Events: Decouples game logging and statistics tracking from core game logic, allowing multiple observers to react to game events.

  5. Command Pattern for Player Actions: Encapsulates each player action (Hit, Stand, Double, Split) as an object, enabling undo functionality and action history tracking.

Comments