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 aHand-level decision. - value =
hard + 10when the hand has an ace andhard + 10 <= 21; elsehard. The+10promotes one ace from 1 to 11 (it was already counted as 1). - soft = that
+10applied. A soft hand cannot bust on the next card (the ace silently drops back to 1). - bust tested on
hard > 21, nevervalue > 21(value is already capped, sovalue > 21only hides a future bug). - Only one ace is ever 11, because
11 + 11 = 22is already a bust. No2^aloop 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 // 2in 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
Handinto two, each keeping the original bet, each settled independently.from_split=Trueblocks 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
Playerinstead 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)takesdrawas 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 seed | 21 as target and bust threshold |
Which DealerPolicy is installed | The blackjack definition |
| Side-bet paytables | The 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;
drawsetspending_shuffle(honoured between rounds, never mid-round). Not a list ofDecks, not a Singleton (inject it for two tables and deterministic tests). - Hi-Lo: 20 cards at
+1(2-6), 12 at0(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
Wagerwith 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
valueonCard(an ace’s value needs the whole hand). - Do not put the bet on
Player, makeShoea 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.