OOD - Blackjack and a Deck of Cards
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:
-
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
-
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)
-
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
-
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)
-
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)
-
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
-
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
- Fairness: Cryptographically secure shuffling, no card counting advantages through predictable patterns
- Performance: Handle 100+ rounds per hour with 7 players, instant hand evaluation
- Auditability: Complete game logs for regulatory compliance, all decisions traceable
- Configurability: Adjustable rules (H17/S17, surrender options, blackjack payout ratios)
- 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
-
State Pattern for Hand Management: Allows hands to transition through states (Active ā Standing/Busted/Blackjack) with proper validation of allowed actions at each state.
-
Chain of Responsibility for Payouts: Separates payout logic into distinct calculators (Blackjack ā Win ā Push ā Loss) that can be easily modified or extended.
-
Strategy Pattern for Dealer Rules: Enables different dealer strategies (H17 vs S17) without modifying the Dealer class, supporting various casino rules.
-
Observer Pattern for Game Events: Decouples game logging and statistics tracking from core game logic, allowing multiple observers to react to game events.
-
Command Pattern for Player Actions: Encapsulates each player action (Hit, Stand, Double, Split) as an object, enabling undo functionality and action history tracking.