InterviewPrepKit

Home / Cheat Sheet / Object-Oriented Design

Cheat sheet

How to design a Blackjack game

Read the full lesson →

Blackjack is a rules engine: the design lives or dies on valuing an ace, putting the bet on the hand, pricing the one variable house rule, and deciding for each rule whether it is data or code.

The ace algorithm (no enumeration)

  • hard = sum of every card with each ace counted as 1. base value lives on Card; the 11 is a Hand-level decision.
  • value = hard + 10 when the hand has an ace and hard + 10 <= 21; else hard. The +10 promotes one ace from 1 to 11 (it was already counted as 1).
  • soft = that +10 applied. A soft hand cannot bust on the next card (the ace silently drops back to 1).
  • bust tested on hard > 21, never value > 21 (value is already capped, so value > 21 only hides a future bug).
  • Only one ace is ever 11, because 11 + 11 = 22 is already a bust. No 2^a loop needed.
hard = sum(base, ace=1)
value = hard + 10  if ace and hard+10 <= 21   (SOFT)
      = hard       otherwise                  (HARD)
bust  = hard > 21

Blackjack, and who gets paid

  • Blackjack = exactly 2 cards, total 21, and not from_split. A 3-card 21 and a split-ace 21 are not blackjacks.
  • Blackjack pays 3:2, coded bet * 3 // 2 in integer cents, floor rounding (state the rounding rule).
  • Win = even money (+bet), push = 0 (tie), loss = -bet. A blackjack over a plain 21 is the reason it must be its own predicate.

The bet lives on Hand, not Player

  • Split turns one Hand into two, each keeping the original bet, each settled independently. from_split=True blocks blackjack on both.
  • bet = original stake, never changes. wager = at risk now, doubles after a double-down. Keep both so reports still read the stake.
  • Insurance is a separate wager (half the bet, 2:1, on a dealer ace, resolved first), not a field on Hand; a 4-hand split must not carry 4 insurance bets. Attach wagers to the seat.
  • Bet on Player instead forces parallel bet/hand lists kept index-aligned, and the first bug pays the wrong hand.

Settlement: order IS the design

First matching clause returns, so precedence is everything.

flowchart TD
    A["Player bust?"] -- yes --> A1["-wager"]
    A -- no --> B["Both blackjack?"]
    B -- yes --> B1["push 0"]
    B -- no --> C["Player blackjack?"]
    C -- yes --> C1["pay 3:2"]
    C -- no --> D["Dealer blackjack?"]
    D -- yes --> D1["-wager"]
    D -- no --> E["Dealer bust?"]
    E -- yes --> E1["+wager"]
    E -- no --> F["Higher total wins, equal pushes"]
  • Player-bust sits first: a busted player loses even if the dealer busts afterward. That asymmetry (player acts and settles first) is the house edge.

Dealer is a Strategy worth real money

  • Dealer makes zero decisions; whole behaviour is DealerPolicy.should_hit(hand) -> bool, a pure predicate.
  • S17 (StandSoft17): value < 17. H17 (HitSoft17): value < 17 or (value == 17 and is_soft). They differ on exactly one hand, A 6.
  • H17 is worth about 0.22 percentage points of house edge, so it must be swappable at construction, not a constant.
  • Dealer.play(hand, draw) takes draw as an argument (dependency injection), so a test can script the cards.

Data vs code

Data (change without recompiling)Code (read as a sentence, type-checked)
Ranks and suits (RANKS, SUITS)The ace hard + 10 algorithm
Deck count, penetration, shuffle seed21 as target and bust threshold
Which DealerPolicy is installedThe blackjack definition
Side-bet paytablesThe order of settlement clauses
Hi-Lo counting tags; bet/doubled/split on Hand
  • Rule of thumb: move predicates into data, leave precedence in code. Predicates compose in any order; settlement’s correctness is its ordering.
  • The 3:2 payout is deliberately hard-coded, not a paytable, until a second payout (6:5) ever ships.

Shoe, counting, side bets

  • Shoe = N decks + cut card at a penetration fraction; draw sets pending_shuffle (honoured between rounds, never mid-round). Not a list of Decks, not a Singleton (inject it for two tables and deterministic tests).
  • Hi-Lo: 20 cards at +1 (2-6), 12 at 0 (7-9), 20 at -1 (10-A) per deck; balanced so a dealt shoe returns to zero. true count = running / remaining_decks. Detection is bet-size-vs-count correlation over a long session, not a threshold.
  • Side bet = a Wager with its own paytable resolved on the deal; any outcome not in the table falls through to a loss. Cheap because insurance already made wagers uniform.

Gotchas / do not

  • Do not put value on Card (an ace’s value needs the whole hand).
  • Do not put the bet on Player, make Shoe a Singleton, or reshuffle mid-round.
  • 21 is not blackjack; a split-ace 21 pays even money, not 3:2.
  • 21 is a literal in four places (value, soft, bust, blackjack); a different target is four edits.
Want the full picture? The lesson has the derivations, worked examples, and diagrams this card compresses into bullets. Read the full lesson →
Report a bug