Design a blackjack game.
Blackjack is the classic rules engine problem: a system whose difficulty is not its algorithms or its scale, but the number of interacting special cases in its domain.
This chapter covers four things:
- Value an ace correctly without enumerating combinations.
- Explain why the bet belongs to the hand rather than to the player.
- Put the one genuinely variable house rule behind an interface, and price that variation in money.
- Decide, for any rule in the system, whether it should be stored as data or written as code.
Every example is runnable Python, and every number is derived rather than asserted.
The nouns are given: card, deck, hand, player, dealer. The design is decided by three things: whether an ace is valued correctly, whether the betting unit is the player or the hand, and whether the dealer’s policy is data or an if.
Chapter 10 is the case where reaching for machinery is the failure. This is the opposite: the rules genuinely branch, and a design that flattens them into one play_round function cannot absorb a single casino variation.
The rules, once, so nothing below needs looking up
This section defines every term used later, so no prior knowledge of blackjack is required.
The table
Blackjack is played against the house, not against the other players. Several players sit at one table and each plays their own hand against a single dealer, who represents the casino. Two players at the same table never compete with each other.
Before any cards are dealt, each player puts money on the table. That stake is the bet.
The deal
Each player receives two cards face up.
The dealer receives two cards as well: one face up, called the upcard, and one face down, called the hole card. The upcard is the only information you have about the dealer while you decide what to do, which is why every strategy table in blackjack is indexed by it.
What the cards are worth
- A number card is worth its number. A seven is 7.
- The jack, queen and king are each worth 10. Together with the actual 10s, these are the ten-valued cards — 16 of the 52 cards in a deck.
- An ace is worth either 1 or 11, whichever the holder prefers. You never have to choose in advance; the ace is simply counted whichever way is better right now.
A hand’s total is the sum of its cards. 9 + 7 is 16. A + 6 is 17, counting the ace as 11 — or 7, counting it as 1. Both readings are live at once, and Decision 1 soft versus hard and the ace algorithm is entirely about resolving that.
The goal is to finish with a total closer to 21 than the dealer’s, without going over 21.
Your turn
Each player acts in turn. On your turn you may hit — take another card — as many times as you like, or stand, meaning take no more and end your turn.
If your total goes above 21 you have bust, and the hand loses immediately. It loses even if the dealer goes on to bust too, and that asymmetry is the main reason the casino has an edge (Decision 2 the hand is the betting unit).
The dealer’s turn
When every player has finished, the dealer turns over the hole card and draws cards according to a fixed, published rule. The dealer never exercises judgement and never reacts to what the players hold. This is the most useful fact about the domain, and Decision 3 the dealer is a strategy and it is worth real money builds a class around it.
Who gets paid
- Beat the dealer’s total and you win even money: you keep your bet and receive the same amount again. A 100-unit bet returns 100 units of profit.
- Tie the dealer and the hand is a push: your bet comes back and nothing is won or lost.
- Lose, and the bet is taken.
- A blackjack — an ace plus a ten-valued card in the opening two cards, for a total of 21 — normally pays 3:2 instead of even money. A 100-unit bet returns 150 units of profit rather than 100.
The “3:2” notation means three units paid for every two staked. 100 x 3 / 2 = 150.
Four optional actions
These four are optional at most tables, and each one bends the model in a way worth noticing now.
- Double (or double down): double the bet in exchange for exactly one more card, then you must stand.
- Split: if your first two cards are a pair, turn them into two separate hands, each with its own bet equal to the original.
- Insurance: a side bet offered only when the dealer’s upcard is an ace. It pays 2:1 if the dealer turns out to have blackjack.
- Surrender: give up half the bet to end the hand immediately.
Where the rest of the series fits
Three links, offered for depth and not needed to follow this chapter. The interview method — how to spend the 45 minutes — is 02 — a framework for the object-oriented design (OOD) interview. The pattern vocabulary is 03 — object-oriented programming (OOP) fundamentals, and the two patterns used here are defined where they arrive: Strategy (a policy moved into its own object so it can be swapped without editing the caller) and Observer (a publisher that emits events to an unknown set of subscribers). Money is handled as integer cents throughout, for the reasons worked out in Money is the first design decision.
What goes in, and what comes out
Fix the shape of the problem before drawing a single class. Underneath the table felt, the system is a function from a hand plus a rule set to an amount of money.
The block below is that function’s signature in English. The three IN lines are everything the system needs to know; the two OUT lines are everything it has to produce. The arrows show a concrete example of each: a player holding an ace and a king with 100 staked, against a dealer who finished on 20, ending with the player up 150.
IN a hand the cards a player holds, and what they staked on them
-> Hand(cards=[A, K], bet=100)
IN a dealer the cards the dealer finished with
hand -> Hand(cards=[10, K])
IN a rule set which house variation is in force
-> Dealer(policy=StandSoft17())
OUT a valuation the total of a hand, and whether it is soft, bust, or a
blackjack
-> value 21, soft, blackjack
OUT a settlement net cents to the player, positive for a win and negative
for a loss
-> +150
The whole design exists to make that last line correct in every case, and there are more cases than there appear to be. A player who busts loses even if the dealer busts afterwards. A 21 built from three cards is not a blackjack. A 21 built from a split ace is not a blackjack either. Each of those is one clause, and getting the clauses in the right order matters more than any class diagram.
1. Clarifying questions that change the design
Five questions change the code, not merely the conversation. Each row pairs one of them with the structural consequence of its answer.
| Question | Effect on the design |
|---|---|
| Does the dealer hit soft 17? | A real table-by-table variation worth measurable house edge, where “house edge” means the casino’s expected profit as a percentage of the amount wagered. It must be a policy object rather than a constant (Decision 3 the dealer is a strategy and it is worth real money) |
| Are split, double, insurance and surrender offered? | Splitting is the one that reshapes the model: one player can be playing four hands with four independent bets. The bet belongs to the hand |
| How many decks, and where is the cut card? | One deck versus six changes the odds and the shuffle lifecycle. It also decides whether card counting is a threat you have to model |
| Several players at one table? | Turn order and a shared shoe. It does not change the hand logic at all, which is a good thing to notice out loud |
| Does blackjack pay 3:2 or 6:5? | Ideally a payout table rather than an if, and integer money either way, so bet * 3 // 2 with a stated rounding rule (Money is the first design decision) |
The code below makes good on the last row only partly, and What is data and what is code is where that gap is named.
2. Core objects, and why those
Each object below is paired with the plausible alternative it was chosen over. The alternatives are all defensible, which is why the reasons matter more than the list.
One term needs defining first. A shoe is the box a dealer draws from, holding several shuffled decks stacked together; six is the usual number. A larger card pool is harder to track and slower to exhaust.
The third column answers “why not do the obvious thing?”, because the obvious thing is what most candidates do.
| Object | Responsibility | Why not the obvious alternative |
|---|---|---|
Card | A rank and a suit, immutable and hashable | Its value is not a property of the card: an ace is 1 or 11 depending on the rest of the hand. Putting value on Card is the first mistake |
Shoe | One to eight shuffled decks, a cut card, a discard tray | Not Deck. A six-deck shoe is not a list of six Deck objects; it is one card sequence with a reshuffle trigger |
Hand | Its cards, its own bet, and its own resolution | Not a field on Player. After a split one player has several hands and the bets are independent |
Player | Bankroll, seat, and the hands currently in play | The bankroll is the player’s total money; the wager is one hand’s |
Dealer | A hand plus a DealerPolicy | The dealer never chooses. Its whole behaviour is one policy object |
DealerPolicy | A single method, should_hit(hand) -> bool | Strategy, worked in Decision 3 the dealer is a strategy and it is worth real money |
Wager / Payout | An amount, its odds, and its resolution | Exists so that insurance and side bets are the same kind of thing as the main bet |
Two terms in that table are worth glossing. Immutable means the object cannot be changed after construction, and hashable means it can be used as a dictionary key or put in a set — the two go together, because a key that can change underneath the dictionary corrupts it. A card is the textbook case: the four of hearts is a value, not a thing with a history. And the cut card is a coloured plastic card inserted part-way into the shoe; when it is reached, the shoe is reshuffled at the end of the current round, which is the mechanism that limits how much of the shoe a card counter ever gets to see.
Hand owning the bet is the structural decision this question is really testing. If the bet lives on Player, splitting requires a parallel list of bets kept in lockstep with a list of hands, and every double-down has to find the right index into both. Interviewers ask about splitting because it breaks the naive model.
3. Class diagram
Those objects assemble into the diagram below. Two notes before reading it.
First, this is the whole model, including parts the chapter names but never codes. Card, Hand, DealerPolicy, Dealer, Shoe and the settlement function all appear as running Python later. Round, Player and Action do not; they are the plumbing, and the decisions live elsewhere.
Second, the lines between the boxes carry the design. Read the diagram once for shape, then read the two lists that follow it.
classDiagram
class Card {
+Rank rank
+Suit suit
+base() int
}
class Shoe {
+int decks
+float penetration
+bool pending_shuffle
+draw() Card
+remaining_decks() float
}
class Hand {
+int bet
+bool doubled
+bool from_split
+hard() int
+value() int
+is_soft() bool
+is_bust() bool
+is_blackjack() bool
}
class Player {
+int bankroll
+List hands
+decide(hand, upcard) Action
}
class Dealer {
+Hand hand
+play(hand, draw)
}
class DealerPolicy {
<<interface>>
+should_hit(hand) bool
}
class StandSoft17
class HitSoft17
class Round {
+deal()
+settle() int
}
class Action {
<<enumeration>>
HIT
STAND
DOUBLE
SPLIT
SURRENDER
}
Round "1" o-- "1" Shoe : shared across rounds
Round "1" *-- "1" Dealer
Round "1" o-- "1..*" Player : seated
Player "1" *-- "1..4" Hand : owns, dies with the round
Dealer "1" *-- "1" Hand
Dealer "1" --> "1" DealerPolicy : delegates
Hand "1" o-- "2..*" Card : drawn from the shoe
DealerPolicy <|.. StandSoft17
DealerPolicy <|.. HitSoft17
Player ..> Action : returns
How to read the notation
This is a UML class diagram. UML is the Unified Modeling Language, the standard set of shapes for drawing software structure.
Inside a box: each box is a class, the lines inside it are its fields and methods, and a leading + means public — visible to code outside the class.
Two boxes carry a stereotype in double angle brackets. <<interface>> marks a class that is only a promise of methods, with no implementation of its own. <<enumeration>> marks a type whose values are a fixed, named, closed list, so HIT and STAND inside the Action box are values, not fields.
The quoted numbers at the ends of a line are multiplicities: how many objects sit at that end. 1 is exactly one, 1..* is one or more, 1..4 is between one and four, 2..* is two or more.
Five line styles appear, and they mean five different things.
*--, a filled diamond, is composition: the owner controls the part’s lifetime, so destroying the owner destroys the part.o--, a hollow diamond, is aggregation: a “has a” relationship that does not control lifetime, so the part can outlive the whole or be shared.-->, a plain open arrow, is a directed association: the class at the tail holds a reference to something it did not create and does not own...>, a dashed open arrow, is a dependency: the class at the tail merely uses or mentions the class at the head.<|.., a hollow triangle on a dashed line, is realization: the class at the tail implements the interface at the head without inheriting any code from it.
Every line, read back as a sentence
One bullet per line in the diagram, in the order they are drawn.
Round o-- Shoe, labelled “shared across rounds”. A round has one shoe but does not own it. That hollow diamond is the point of a shoe: the cards left in it carry over into the next round, which is what makes card counting possible.Round *-- Dealer. A round owns exactly one dealer, who exists for the duration of that round and no longer.Round o-- Player, labelled “seated”. One or more players, not owned. People walk up to a table and walk away from it.Player *-- Hand, labelled “owns, dies with the round”. Between one and four hands, owned outright. Hands are created at the deal, split into more hands mid-round, and destroyed at settlement. The upper bound of four is the common house limit on re-splitting: three splits at most.Dealer *-- Hand. The dealer owns exactly one hand of their own, and never splits it.Dealer --> DealerPolicy, labelled “delegates”. A plain arrow, not a diamond, because the policy is handed to the dealer at construction rather than created by it.Hand o-- Card, labelled “drawn from the shoe”. Two or more cards, not owned. Aggregation is right because the cards came from somewhere else and go to a discard tray afterwards.DealerPolicy <|.. StandSoft17and<|.. HitSoft17. Each concrete policy implements the interface. This is the claim that the dealer can hold either one and never know which.Player ..> Action, labelled “returns”. A player produces action values without containing any.
The Round o-- Shoe versus Player *-- Hand contrast is the pair worth getting right, because reversing either says something false: a shoe that died with the round would make counting meaningless, and a hand that outlived the round would be money left on the table.
What this class structure assumes
A class diagram is a fixed bet about what will change. Every interface says “I expect this to vary”; every literal written into a method says “I expect this to hold forever”. Stating those bets is the transferable skill. The general form of the argument is What a class structure assumes; what follows is this design’s version.
Assumed to vary — and therefore given an interface, a parameter, or a table. The third column is the price of the alternative: what the code would look like if this had been hard-coded instead.
| What varies | How the design absorbs it | What it would cost to have got this wrong |
|---|---|---|
| The dealer’s drawing rule | DealerPolicy, an interface with one method | A boolean flag threaded through the draw loop, and a second flag for the next variation |
| How many decks, and how deep they are dealt | decks and penetration constructor arguments on Shoe | A 52-card constant, and single-deck odds silently applied to a six-deck game |
| Where the shuffled order comes from | An injected random number generator | No deterministic test is possible, so no rule can be tested against a known deal |
| What a player decides to do | Player.decide, returning an Action | The round loop grows a branch per kind of player: human, basic strategy, counter |
| Side-bet payouts | A paytable, stored as data on the Wager | Every new side bet is a new method on the settlement path |
| How many hands a player holds | A list on Player, with the bet on each Hand | Split becomes parallel lists indexed in lockstep |
Assumed fixed — and therefore written into the structure rather than into a parameter:
The largest one contradicts a clarifying question, and it is better to say so than to let an interviewer find it. The 3:2 blackjack payout is written directly into the settlement code as bet * 3 // 2, even though Clarifying questions that change the design argues it should be a payout table. That is a deliberate trade: a table earns its keep once there are several payouts to vary together, and until then one arithmetic expression is easier to read and harder to mis-key. If a 6:5 table ever ships, the fix is one payout table consulted once, not a second branch.
Underneath that sit six more fixed bets. Each is a place where the code would have to be edited, not configured.
- 21 is written as a literal in four separate places — the value calculation, the soft test, the bust test, and the blackjack test. A variant with a different target is four edits rather than one constant.
- A card’s base value is fixed in
Card.base. A variant where a five is worth something unusual has no seam to change. - The settlement clauses are ordered control flow, not a rule table, so their precedence is invisible to anything except reading them in order.
- The action set is a closed enum, so an offer like “even money”, or a limit on re-splitting aces, has nowhere to live.
- The dealer holds exactly one hand and never splits. True of every real table, but an assumption nonetheless.
- Money is integer cents with floor rounding. State this one out loud. The alternative rounding rule moves the house edge by a fraction of a cent per hand, in the casino’s favour, forever.
What a different assumption would have produced.
- If the bet lived on
Player, split would need a list of bets kept index-aligned with a list of hands. Every double, every surrender and every settlement would then be an index lookup into two structures that can drift apart, and the first bug is paying the wrong hand. - If
Shoewere a Singleton — the pattern that permits only one instance of a class to exist — a casino could not run two tables in one process and your test suite could not build a deterministic shoe. That is the standard trap, and the answer is dependency injection: hand the shoe in (chapter 03). - If insurance were a field on
Hand, a player holding four split hands would hold four insurance bets, which is not the rule. Insurance is offered once per seat, against the original wager, on a dealer ace. - If the entire rule set were data — a table of conditions and outcomes rather than methods — you would get an engine that runs Spanish 21, pontoon and double-exposure blackjack from configuration alone. That is a real product and a real cost, and the next section prices it.
What is data and what is code
Blackjack is a rules engine, so the question worth answering explicitly is which rules are stored as values you can change without recompiling, and which are written as control flow. A rule expressed as data can be edited, versioned and shipped by someone who cannot read the code; a rule expressed as code can be read as a sentence and checked by a type checker. You pay for whichever one you did not pick.
Every rule in this chapter, sorted by which side of that line it falls on. The data rules come first, the code rules after; the last row is the decisive one.
| Rule | Where it lives | Consequence |
|---|---|---|
| Which ranks and suits exist | Data: the RANKS and SUITS lists | A Spanish deck with no tens is a one-line change |
| Deck count, penetration, shuffle order | Data: constructor arguments on Shoe | Single-deck and six-deck games are the same class |
| Whether the dealer draws on soft 17 | Data-shaped code: which DealerPolicy object is installed | The variation worth 0.22 percentage points is a constructor argument |
| Side-bet payouts | Data: a tuple of outcome-and-multiplier pairs | A new side bet is a new constant, not a new code path |
| The bet, and whether a hand was doubled or split | Data: fields on Hand | Settlement reads state instead of remembering history |
| Hi-Lo counting tags | Data: a mapping from rank to +1, 0 or -1 | A different counting system is a different mapping |
| An ace is 1 or 11 | Code: the hard + 10 calculation | Correct, fast, and unchangeable without an edit |
| 21 is the target and the bust threshold | Code: a literal in four properties | A different target is four edits |
| What counts as a blackjack | Code: two cards, 21, and not from a split | Casino-specific exceptions have no seam |
| The order settlement clauses are tested in | Code: the sequence of if statements in settle | This is the one that must not become data |
The dealer’s rule became an object almost for free; the settlement order could not, and the difference between those two cases is the real lesson. should_hit is a pure predicate: a function of one hand, returning a yes or a no, with no dependence on anything else in the system and no interaction with any other rule. Predicates like that generalize to data cheaply, because a table of them composes in any order. Settlement is the opposite. Its correctness is the ordering — the player-bust clause must be tested before the dealer-bust clause, or the house edge disappears — and orderings expressed as table rows are exactly the thing that goes wrong, as Decision 1 promotions are strategies and they do not commute works through at length in a different domain.
So the rule of thumb, worth saying in the interview: move predicates into data, leave precedence in code. A fully table-driven blackjack engine stops producing stack traces you can read. A bug becomes “row 14 of the settlement table fired before row 9”, the type checker can check nothing, and each rule is no longer a sentence anybody can review. The opposite mistake, leaving everything in code, means every casino variation is a code deployment. The design here sits deliberately near the code end, and the one thing that had to move to the data end moved: the dealer’s policy, because that is the variation with money attached. The mirror-image judgement for a problem where almost nothing should move is What is data and what is code.
4. Decision 1 — soft versus hard, and the ace algorithm
The whole ace rule follows from one line of arithmetic, plus two subtleties that break real implementations. A table of hands proves all of it.
An ace is worth 1 or 11, whichever helps. The naive implementation tries every combination: with a aces in the hand that is 2^a different sums to evaluate, 2 sums for one ace, 8 for three, 2048 for eleven. None of that is needed, and the reason is one line of arithmetic:
two aces valued 11 each 11 + 11 = 22
22 is already a bust, so at most one ace in any hand can ever count as 11. No matter how many aces you hold, all but one of them are worth 1.
That collapses the whole problem into arithmetic with no loops and no 2^a. Here it is as pseudocode; the next block is the same thing as running Python.
flowchart TD
A["Sum every card, ace = 1: hard"] --> B{"Hand has an ace and hard + 10 fits under 21?"}
B -- yes --> C["value = hard + 10, hand is SOFT"]
B -- no --> D["value = hard, hand is HARD"]
C --> E{"hard over 21?"}
D --> E
E -- yes --> F["bust"]
E -- no --> G["in play"]
hard = sum of card values with every ace counted as 1
if the hand has an ace and hard + 10 <= 21:
value = hard + 10 and the hand is SOFT
else:
value = hard and the hand is HARD
bust = hard > 21 (never `value > 21` -- value is already capped)
The + 10 is the promotion of one ace from 1 to 11. It is a gain of 10 rather than of 11 because that ace was already counted once, as a 1, inside hard.
Work one hand through it. A 6: hard is 1 + 6 = 7. There is an ace and 7 + 10 = 17 fits under 21, so value is 17 and the hand is soft. Now add a 10. hard is 17, 17 + 10 = 27 does not fit, so value stays 17 and the hand is hard. The ace demoted itself with no code doing anything special.
That is what soft means: the promotion applied. The name matters because a soft hand cannot bust on the next card. If the extra card would take it over 21, the promoted ace silently drops back to 1, exactly as it just did. A player holding soft 17 has a free hit; a player holding hard 17 does not. That single property is what basic strategy and the dealer’s own policy both branch on.
Two subtleties trip implementations, and both are worth volunteering.
- Bust must be tested on
hard, not onvalue. Becausevaluealready refuses to exceed 21, the expressionvalue > 21is only ever true whenhard > 21anyway — so it appears to work. Writing it that way invites the later bug where somebody “fixes”valueto be uncapped and the bust test silently changes meaning. - 21 is not blackjack. A blackjack is exactly two cards totalling 21 and not produced by a split, because a split ace joined by a ten pays even money rather than 3:2. That is a rule, and it lives on
Hand.
The test set that proves it
Two blocks. This first one is the model — Card and Hand, with the pseudocode above written out as the hard, value, is_soft and is_bust properties. The second block is the table of hands that proves those properties right.
Notice that Card has no value. It has base, which counts an ace as 1, and the decision to promote one ace to 11 lives on Hand where the rest of the cards are visible. That split is the whole point of Core objects and why those’s first row.
from dataclasses import dataclass, field
from enum import Enum
from typing import List
class Suit(Enum):
SPADES = "S"
HEARTS = "H"
DIAMONDS = "D"
CLUBS = "C"
@dataclass(frozen=True)
class Card:
rank: str
suit: Suit = Suit.SPADES
@property
def base(self) -> int:
"""Value with an ace counted as 1. The 11 is a HAND-level decision."""
if self.rank == "A":
return 1
return 10 if self.rank in ("10", "J", "Q", "K") else int(self.rank)
@dataclass
class Hand:
cards: List[Card] = field(default_factory=list)
bet: int = 0 # integer cents; the bet belongs to the HAND
doubled: bool = False
from_split: bool = False
@property
def hard(self) -> int:
return sum(c.base for c in self.cards)
@property
def aces(self) -> int:
return sum(1 for c in self.cards if c.rank == "A")
@property
def value(self) -> int:
"""At most one ace can be 11, because 11 + 11 = 22 is already a bust."""
h = self.hard
return h + 10 if self.aces and h + 10 <= 21 else h
@property
def is_soft(self) -> bool:
return bool(self.aces) and self.hard + 10 <= 21
@property
def is_bust(self) -> bool:
return self.hard > 21 # NOT value > 21: value is already capped
@property
def is_blackjack(self) -> bool:
return len(self.cards) == 2 and self.value == 21 and not self.from_split
@property
def wager(self) -> int:
return self.bet * 2 if self.doubled else self.bet
def can_split(self) -> bool:
return (len(self.cards) == 2
and self.cards[0].base == self.cards[1].base)
The Python idioms in that block
Skip this if the syntax is already familiar; nothing in it is blackjack.
@dataclass is a decorator that writes a class’s constructor, its equality test and its printable form from the annotated fields alone. frozen=True on Card makes instances immutable and hashable, which is exactly the “a card is a value” claim from Core objects and why those enforced by the language. Hand is not frozen, because a hand grows. field(default_factory=list) gives each new hand its own empty list: writing cards: List[Card] = [] instead would share one list between every hand ever created, which is a classic Python bug, and dataclasses refuse it outright. @property turns a method into an attribute read, so hand.value computes on access rather than being a stored field that can go stale — which is the right choice here, since the value of a hand is always a function of its cards. sum(c.base for c in self.cards) is a generator expression, summing a computed value over the cards without building an intermediate list; sum(1 for c in ... if ...) is the standard way to count matches. Suit.SPADES as a default means every card in the tests below is a spade, which is harmless because nothing here depends on suit.
The hands
Now the second block. CASES is a table written as Python: each row is a set of ranks, then the three answers that hand must produce — its hard total, its value, and whether it is soft. The loop underneath checks all three on every row, plus the bust test.
The rows are chosen to cover the ace rule’s corners: one ace, two aces, three aces, a hand where the ace is forced back down to 1, and one absurd hand with eleven aces. The asserts after the loop are a separate point: 21 and blackjack are not the same thing.
def h(*ranks: str, **kw) -> Hand:
return Hand([Card(r) for r in ranks], **kw)
# ranks hard value soft?
CASES = [
(("A", "6"), 7, 17, True), # the canonical soft 17
(("A", "6", "10"), 17, 17, False), # the ace demoted to 1
(("A", "A"), 2, 12, True), # only one ace can be 11
(("A", "A", "9"), 11, 21, True),
(("A", "A", "A", "8"), 11, 21, True), # three aces, still one 11
(("A", "A", "9", "K"), 21, 21, False), # forced hard 21
(("A", "5", "5"), 11, 21, True),
(("A", "K"), 11, 21, True), # blackjack, and soft
(("A", "2", "3", "4", "5"), 15, 15, False),
(("9", "9", "4"), 22, 22, False), # bust
(tuple("A" * 11), 11, 21, True), # eleven aces is exactly 21
]
for ranks, hard, value, soft in CASES:
hd = h(*ranks)
assert hd.hard == hard, (ranks, hd.hard)
assert hd.value == value, (ranks, hd.value)
assert hd.is_soft is soft, (ranks, hd.is_soft)
assert hd.is_bust is (hard > 21), ranks
# 21 is not the same thing as blackjack.
assert h("A", "K").is_blackjack
assert not h("A", "5", "5").is_blackjack # three cards
assert not h("A", "A", "9").is_blackjack # three cards
assert not h("A", "K", from_split=True).is_blackjack # split ace + ten
assert not h("8", "9").can_split()
The helper h uses two Python conventions worth naming. *ranks collects every positional argument into a tuple, so h("A", "K") passes ("A", "K"); **kw collects every keyword argument into a dictionary and forwards it unchanged, which is how h("A", "K", from_split=True) reaches the Hand constructor. And tuple("A" * 11) builds eleven separate single-character strings, because multiplying a string repeats it and tuple splits the result into characters — a compact way to write out an eleven-ace hand. An assert raises immediately if its condition is false, so a block of them that runs to completion is a passing test; the value after the comma is the message printed if it fails.
Two of those rows earn their place. A A A 8 has three aces, and exactly one of them is promoted, so the hand is hard 11 and value 21. The eleven-ace hand is hard 11 and value 21 as well, the extreme statement of the same rule. Naming a hand like A A A 8 before the interviewer does shows you have tested the corner cases.
5. Decision 2 — the hand is the betting unit
Split, double, insurance and surrender all fall out of a single modelling choice, the same choice that makes the settlement function below possible.
The choice is one sentence: the bet is a field on Hand, not on Player. Here is what each of the four optional actions costs once you have made it.
| Action | What it does to the model |
|---|---|
| Split | One Hand becomes two, each keeping the original bet, each drawing a second card, each played and settled independently. from_split=True on both, so neither can be a blackjack |
| Double | doubled=True, exactly one more card, then a forced stand. The wager doubles; the bet field does not, so the original stake is still recoverable for reporting |
| Insurance | A separate wager of half the main bet, paying 2:1, resolved before the main hand plays. It is not an attribute of the hand — it is a second bet on a different proposition |
| Surrender | Return half the wager and end the hand. One early-exit branch |
Notice that bet and wager are deliberately two different things. bet is what was originally staked and never changes; wager is what is actually at risk right now, which is double after a double-down. Keeping both means a report can still say “this player bets 100 a hand” after a session full of doubles.
Insurance is the one that exposes a weak model. If a Hand carries an insurance field, then a player with four split hands has four insurance bets, which is not the rule — insurance is offered once, on the dealer’s ace, against the original wager. Modelling all wagers uniformly (main, insurance, side bet) and attaching them to the seat at the table rather than to a hand makes that fall out correctly, and makes the side bets of Now add side bets nearly free.
Settlement, clause by clause
Settlement is six clauses, and the order they are tested in is the design. Read them top to bottom; each one returns, so the first that matches wins.
- Player bust — lose, before the dealer has drawn anything.
- Both have blackjack — push.
- Player has blackjack — pay 3:2.
- Dealer has blackjack — lose.
- Dealer bust — win.
- Otherwise compare totals: higher wins, lower loses, equal pushes.
The asserts underneath the function are nine worked hands, one per rule that people get wrong.
def settle(player: Hand, dealer: Hand) -> int:
"""Net cents to the player. Positive is a win, negative is a loss."""
w = player.wager
if player.is_bust:
return -w # settled before the dealer even draws
if player.is_blackjack and dealer.is_blackjack:
return 0
if player.is_blackjack:
return player.bet * 3 // 2 # 3:2, floored. State the rounding rule.
if dealer.is_blackjack:
return -w
if dealer.is_bust:
return w
return w if player.value > dealer.value else (
-w if player.value < dealer.value else 0)
assert settle(h("A", "K", bet=100), h("10", "K")) == 150 # 3:2
assert settle(h("A", "K", bet=100), h("A", "Q")) == 0 # both blackjack
assert settle(h("10", "K", bet=100), h("A", "Q")) == -100
assert settle(h("10", "9", "5", bet=100), h("10", "8", "6")) == -100 # both bust
assert settle(h("10", "K", bet=100), h("10", "8", "6")) == 100
assert settle(h("10", "K", bet=100), h("10", "K")) == 0 # push
assert settle(h("A", "K", bet=100, from_split=True), h("10", "K")) == 100
assert settle(h("10", "K", bet=100, doubled=True), h("10", "9")) == 200
assert settle(h("10", "9", bet=100, doubled=True), h("10", "K")) == -200
bet * 3 // 2 uses //, floor division, which discards the remainder rather than producing a fraction. On a 100-cent bet that is exactly 150; on an odd bet it rounds against the player, and the point is not which way it rounds but that the rule is stated at all — an unstated rounding rule is how a cent per hand quietly becomes a lawsuit. The final return is a nested conditional expression, which reads as: the wager if the player’s total is higher, the negative wager if it is lower, and zero if they are equal, that last case being the push.
The fourth assertion is the rule people get wrong: the player busted with 24, so the hand was already lost, and the dealer’s later bust with 24 does not save it. That asymmetry, the player acting first and being settled first, is the reason the house has an edge.
The seventh assertion is the split payoff. The player holds an ace and a king for 21, which would normally be a blackjack paying 150, but from_split=True blocks it, so the hand beats the dealer’s 20 and pays 100.
6. Decision 3 — the dealer is a Strategy, and it is worth real money
This is the one place in the design where an interface is unambiguously correct, and the variation behind it can be priced, so the choice is justified in money rather than in taste.
The interface, and the two policies behind it
The dealer makes no decisions. should_hit is the entire behaviour, and there are two real-world versions, known at the tables as S17 and H17 — stand on soft 17, and hit soft 17.
The difference is one hand: A 6, which is soft 17. An S17 dealer stands on it. An H17 dealer takes one more card. Every other total is played identically by both. That is the entire variation, and the second half of this section shows it is worth real money anyway.
from abc import ABC, abstractmethod
class DealerPolicy(ABC):
@abstractmethod
def should_hit(self, hand) -> bool:
...
class StandSoft17(DealerPolicy):
"""S17. Better for the player."""
def should_hit(self, hand) -> bool:
return hand.value < 17
class HitSoft17(DealerPolicy):
"""H17. The dealer takes one more card on A-6."""
def should_hit(self, hand) -> bool:
return hand.value < 17 or (hand.value == 17 and hand.is_soft)
class Dealer:
def __init__(self, policy: DealerPolicy):
self.policy = policy
def play(self, hand, draw) -> None:
while self.policy.should_hit(hand) and not hand.is_bust:
hand.cards.append(draw())
ABC stands for abstract base class, Python’s way of declaring a class that exists only to be subclassed; @abstractmethod marks a method that a subclass must supply, and attempting to instantiate a class that has not supplied it raises a TypeError rather than failing later in mysterious ways. The ... in the method body is the ellipsis literal, used here as a placeholder statement meaning “no implementation”. Dealer.play takes draw as an argument — a function that returns the next card — rather than reaching for a shoe itself, which is dependency injection: the dealer is handed what it needs instead of finding it, so a test can pass a function returning a scripted sequence of cards.
Run both dealers on the same cards
Injecting draw means you can hand both dealers the identical deal and watch the rule diverge. Below, both start on A 6 — soft 17 — and both would receive a 5 and then a 6 if they asked for cards.
Trace the H17 dealer by hand before reading the asserts. A 6 is soft 17, so it hits. The 5 makes A 6 5, hard 12: the ace can no longer be 11, so the total is 12, which is under 17, so it hits again. The 6 makes hard 18, and it stands. The S17 dealer never draws at all.
def scripted(cards):
"""A draw() that deals a fixed sequence, so a test is reproducible."""
it = iter(cards)
return lambda: next(it)
s17_hand = h("A", "6")
h17_hand = h("A", "6")
assert s17_hand.value == 17 and s17_hand.is_soft # both start on soft 17
assert StandSoft17().should_hit(s17_hand) is False
assert HitSoft17().should_hit(h17_hand) is True
Dealer(StandSoft17()).play(s17_hand, scripted([Card("5"), Card("6")]))
Dealer(HitSoft17()).play(h17_hand, scripted([Card("5"), Card("6")]))
assert len(s17_hand.cards) == 2 and s17_hand.value == 17 # stood at once
assert len(h17_hand.cards) == 4 and h17_hand.value == 18 # drew the 5, then the 6
# Same player, same cards, different table rule: a win becomes a push.
assert settle(h("10", "8", bet=100), s17_hand) == 100
assert settle(h("10", "8", bet=100), h17_hand) == 0
Those last two lines are the whole argument in miniature. A player standing on 18 beats the S17 dealer’s 17 and pushes against the H17 dealer’s 18. One line of policy, one hundred cents.
What the variation is worth
Now the money. The block below is the exact consequence of the rule, computed by recursion over an infinite deck — ranks 2 through 9 and the ace at 1/13 each, ten-valued cards at 4/13 because four of the thirteen ranks are worth ten — for the dealer showing an ace.
Each row is one way the dealer’s hand can finish. The first two columns are the probability of that ending under each rule; the third is H17 minus S17. Look at the final 17 row first, since it is the one the rule change is aimed at, then at where those 7.33 points went.
S17 H17 change
bust 0.1153 0.1389 +0.0236
final 17 0.1308 0.0575 -0.0733
final 18 0.1308 0.1432 +0.0124
final 19 0.1308 0.1432 +0.0124
final 20 0.1308 0.1432 +0.0124
final 21 0.3616 0.3740 +0.0124
The S17 and H17 columns are each a probability distribution over how the dealer’s hand finishes, so each sums to 1 up to the rounding shown. An “infinite deck” means every draw is treated as independent with fixed rank probabilities, which is the standard simplification: it ignores the small effect of cards already dealt and gets the answer right to within a few hundredths of a percentage point.
H17 busts 2.36 points more often, which sounds good for the player. It is not. The 7.33 points of dealer-17 that disappear do not all turn into busts — only 2.36 do. The other 4.97 turn into 18, 19, 20 and 21, which are hands that beat or tie the player.
Work it through for a player standing on 18 against a dealer ace. A dealer bust or a dealer 17 is a win for that player; a dealer 19, 20 or 21 is a loss; a dealer 18 is a push, which is why it appears in neither column below.
S17 win 0.1153 + 0.1308 = 0.2461
lose 0.1308 + 0.1308 + 0.3616 = 0.6232
EV 0.2461 - 0.6232 = -0.3771
H17 win 0.1389 + 0.0575 = 0.1964
lose 0.1432 + 0.1432 + 0.3740 = 0.6604
EV 0.1964 - 0.6604 = -0.4640
EV is expected value: the average outcome per unit staked, so -0.3771 means losing about 37.71 percent of the bet on average in that spot. The remaining probability in each case is the dealer also finishing on 18, which is a push and contributes nothing. The difference between the two rule sets is:
-0.4640 - -0.3771 = -0.0869
That is 8.69 percent of the bet, on that one cell of the strategy table. A percentage point is one hundredth, so 8.69 points of a 100-unit bet is 8.69 units. Weighted by how often an ace is the upcard, which is one rank in thirteen, that single cell alone is worth:
0.0869 / 13 = 0.0067
That is 0.67 percentage points if every player hand behaved like a stand-on-18. They do not, since most hands face a different upcard and play differently, and the published aggregate effect of H17 across all upcards under optimal play is about 0.22 percentage points of house edge. Either way the conclusion holds: this is a real rule variation with real money attached, so it must be swappable at construction rather than a constant inside a method.
What Strategy costs here: essentially nothing, which is why it is the right call. The interface has one method, there is no shared state, and the two implementations are three lines each. The player is different. Player.decide looks like the same shape but is not: basic strategy, the published lookup table of the mathematically best action for every combination of player hand and dealer upcard, is three tables; card counting adds a running count carried between hands; a human player is input and output. That one does deserve a Strategy of its own, and it is the second one to name.
7. Extension scenarios
Three follow-ups, each stated the way an interviewer states it. For each one the useful answer has the same parts: what changes, what does not, why the design absorbed it, and what it still costs.
“Now support multiple decks and a cut card”
What changes. Shoe gains a decks count, a penetration fraction, and a flag saying a shuffle is due. Nothing else moves.
What does not change. Hand, settle, DealerPolicy and Round are untouched. Cards come from shoe.draw() and always did.
Penetration is the fraction of the shoe that gets dealt before the cut card is reached — 0.75 means three quarters of the cards are used and the last quarter is never seen. It is the single most important number to a card counter, because a count is only worth acting on once a meaningful share of the shoe is known.
In the code below, watch two things: draw sets a flag rather than reshuffling, and every number in the asserts is derived from decks and penetration rather than hard-coded. Six decks is 312 cards; a penetration of 0.75 leaves a quarter of them — 78 — behind the cut card, so the flag trips after 234 draws.
One simplification to be aware of: this standalone Shoe deals (rank, suit) tuples rather than Card objects, so it can be read without the §4 block in scope. A full implementation returns Card, as the class diagram says.
import random
from typing import List, Optional
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
SUITS = ["S", "H", "D", "C"]
class Shoe:
"""N decks with a cut card at `penetration`. Reshuffle between rounds only."""
def __init__(self, decks: int = 6, penetration: float = 0.75,
rng: Optional[random.Random] = None):
self.decks, self.penetration = decks, penetration
self.rng = rng or random.Random()
self.shuffle()
def shuffle(self) -> None:
self.cards: List[tuple] = [(r, s) for _ in range(self.decks)
for r in RANKS for s in SUITS]
self.rng.shuffle(self.cards)
self.cut_at = int(len(self.cards) * (1 - self.penetration))
self.pending_shuffle = False
def draw(self) -> tuple:
if not self.cards:
raise RuntimeError("dealt past the shoe; cut card was ignored")
card = self.cards.pop()
if len(self.cards) <= self.cut_at:
self.pending_shuffle = True # honoured at END of round, not now
return card
def remaining_decks(self) -> float:
return len(self.cards) / 52.0
shoe = Shoe(decks=6, penetration=0.75, rng=random.Random(7))
assert len(shoe.cards) == 312 # 6 * 52
assert shoe.cut_at == 78 # 312 * 0.25
for _ in range(234): # 312 - 78
shoe.draw()
assert shoe.pending_shuffle and len(shoe.cards) == 78
assert shoe.remaining_decks() == 1.5 # 78 / 52, and the count divisor
rng is a random number generator passed in from outside, with rng or random.Random() supplying a fresh one only when the caller did not. Constructing it with random.Random(7) seeds it, meaning the same sequence of “random” cards comes out every run — which is what makes the assertions above reproducible and what makes any rules test possible at all. Optional[random.Random] is a type hint meaning “a generator or None”. The nested comprehension builds every combination of deck copy, rank and suit, giving 6 x 13 x 4 = 312 cards, and self.cards.pop() takes the last one, treating the list as a stack. int(...) truncates toward zero, so cut_at is 78 exactly.
The design point is pending_shuffle, not the shuffle. Reshuffling the instant the cut card appears would change the composition of the shoe in the middle of a round, which is both a rules violation and a race against any player who is mid-decision. The flag defers the reshuffle to the round boundary. A candidate who models the cut card as “shuffle when we run low” has not read the rule.
What deck count is worth
The odds shift from deck count is small and real. The probability that the opening two cards are a blackjack, counting both orders of ace-then-ten and ten-then-ace, is:
1 deck 2 * 4 * 16 / (52 * 51) = 0.0483
6 decks 2 * 24 * 96 / (312 * 311) = 0.0475
The numerators read as: 2 for the two orders, then the number of aces, then the number of ten-valued cards — 4 aces and 16 tens in one deck, 24 and 96 in six. The denominators are the ways to draw two cards in order.
0.0483 - 0.0475 = 0.0008
A blackjack pays 0.5 extra units over a normal win, so 0.0008 x 0.5 is about 0.04 percentage points of house edge from deck count alone, before any counting effects. That is small next to H17’s 0.22, and knowing which is which is the point.
What it costs. The shoe now has a state that must be honoured by whoever drives the round, and nothing in the type system enforces it: if the round loop forgets to check pending_shuffle, the shoe deals past the cut card until it raises. That is why draw raises a RuntimeError with an explicit message rather than returning None.
“Now add card-counting detection”
Card counting is the practice of tracking which cards have already been dealt in order to bet more when the remaining shoe is rich in tens and aces, which is when the player’s edge is highest. It is not cheating and it is not illegal; casinos simply want to know who is doing it.
What changes. An observer on the shoe maintains a running count, plus a per-seat record pairing the count at the moment of each bet with the size of that bet. Detection is a correlation, not a threshold.
What does not change. Nothing in the game. The detector reads the same draw() events the game already produces, which is the one place Observer genuinely earns its keep in this chapter — the set of listeners (pit surveillance, analytics, the shoe’s own tracker) is not known when Shoe is written, which is exactly the condition chapter 10 says the pattern requires and does not find.
Hi-Lo is the standard counting system: every card dealt adds a tag to a running total, +1 for the low cards, 0 for the middle, -1 for the high ones.
The block below is the tag assignment. Each line is a rank group, the number of cards that group has in one 52-card deck, and the tag each of those cards carries — and the +1 group and the -1 group are exactly the same size:
2-6 5 ranks x 4 suits = 20 cards at +1
7-9 3 ranks x 4 suits = 12 cards at 0
10-A 5 ranks x 4 suits = 20 cards at -1
20 + 12 + 20 = 52
The last line is the check that the three groups account for a whole deck. Twenty cards tagged +1 and twenty tagged -1 per deck means a fully dealt shoe returns the running count to zero — so a count that is high partway through is genuine information about what is left, not an artefact of the tagging.
The running count is the sum of those tags over every card dealt so far. It starts at 0 after a shuffle and moves up when small cards come out, because small cards leaving means the shoe that remains is rich in tens and aces — good for the player.
A running count of +8 means something very different with six decks left than with one, so the true count normalizes it by the decks still in the shoe:
true = running / remaining_decks
That divisor is exactly what Shoe.remaining_decks() returns. Using the shoe from the block above, which stopped at the cut card with 78 cards left:
running count +8
remaining_decks() 78 / 52 = 1.5
true count 8 / 1.5 = 5.33
A true count of 5.33 is a strong player advantage; the same +8 running count with six decks still to come would be a true count of 1.33 and worth almost nothing. This division is the whole reason remaining_decks exists on Shoe.
A counter’s tell is not the count itself — it is the bet spread correlated with it. A basic-strategy player bets the same amount every hand; a counter bets one unit when the true count is 1 or below and eight to twelve units when it is 4 or above. The detection signal is therefore the correlation between the true count at the moment of the bet and the size of that bet, measured over a few hundred hands. Correlation here means the ordinary statistical measure of whether two quantities rise and fall together, running from -1 to +1, with 0 meaning no relationship. The honest caveat is that random variation over 100 hands makes the measurement unusable — this is a long-session statistic, and treating it as a per-shoe alarm produces false accusations.
“Now add side bets”
A side bet is an optional extra wager on a proposition other than beating the dealer — for example that your first two cards are a pair.
What changes. A Wager with its own paytable, resolved against the first two cards plus the dealer’s upcard, before the main hand plays.
What does not change. settle, DealerPolicy and Shoe are untouched. Side bets resolve on the deal and never interact with hitting or standing.
Why it is cheap: Decision 2 the hand is the betting unit already made insurance a separate wager rather than a field on Hand. A side bet is the same shape with a different paytable, so it is one dataclass and one resolver.
The example below is a pair side bet. Its three outcomes are ranked by how unlikely they are: a suited pair is two cards of the same rank and the same suit, a coloured pair is the same rank and the same colour but different suits, and a mixed pair is the same rank in different colours. Rarer pays more.
Read the payout method for its fall-through. Any outcome not named in the paytable loses the stake.
from dataclasses import dataclass
from typing import Tuple
@dataclass
class Wager:
amount: int # integer cents
paytable: Tuple[Tuple[str, int], ...] # (outcome, numerator) at :1 odds
def payout(self, outcome: str) -> int:
for name, num in self.paytable:
if name == outcome:
return self.amount * num
return -self.amount
PAIR_PLUS = (("suited pair", 25), ("coloured pair", 12), ("mixed pair", 6))
w = Wager(500, PAIR_PLUS)
assert w.payout("suited pair") == 12500
assert w.payout("mixed pair") == 3000
assert w.payout("no pair") == -500
The paytable is a tuple of pairs, each naming an outcome and the multiplier it pays at odds of that-many-to-one: a suited pair returns 25 times the 500-cent stake, which is 12,500 cents. Any outcome not listed falls through the loop and loses the stake, which is why payout("no pair") is -500 — the default is a loss, and encoding it as the fall-through rather than as a table row is what stops a missing row from silently paying out. Tuple[Tuple[str, int], ...] is the type hint for “a tuple of any length whose elements are all string-and-integer pairs”; the ... here means “repeated”, not “unimplemented”.
What it costs. Side bets have far worse odds than the main game and are resolved against a different set of information, so they must not share the main bet’s settlement path or the accounting silently mixes two different edges into one number. Keep them in a separate list on the seat with their own ledger lines.
8. What interviewers probe
Everything above, in the form it actually arrives: a question, and the answer that ends the follow-up.
| Probe | Answer that lands |
|---|---|
| “How do you value an ace?” | Count all aces as 1, then add 10 once if it still fits under 21. At most one ace can be 11 because 11 + 11 = 22. The hand is soft if that +10 applied |
“Show me A A A 8” | Hard 11, value 21, soft. Three aces, still exactly one counted as 11 |
“Is A K after a split a blackjack?” | No. Two cards and 21, but from_split blocks it, so it pays even money and not 3:2 |
| “Player busts and dealer busts — who wins?” | The dealer. The player acts first and is already settled. That asymmetry is the house edge |
| “Where does the bet live?” | On the Hand. Split creates two hands with two independent bets; putting the bet on Player forces parallel lists that drift |
| “Insurance?” | A separate wager at 2:1 on half the main bet, offered once per seat on a dealer ace, resolved before the hand plays. Not a field on Hand |
| “H17 versus S17 — does it matter?” | Yes. The dealer-ace distribution moves 7.33 points out of “final 17”, and a player standing on 18 loses 8.69 more points of the bet. The published aggregate is about 0.22 points of house edge |
| “When do you reshuffle?” | At the round boundary after the cut card has been passed, never mid-round. pending_shuffle is a flag, not an action |
| “Should the Shoe be a Singleton?” | No. A casino has many tables and your test suite needs a seeded deterministic shoe. Inject a Shoe built with an injected random.Random(seed) (chapter 03) |
| “Which rules are data and which are code?” | Deck count, penetration, paytables and the dealer’s policy are data; the ace algorithm, the 21 threshold and the settlement precedence are code. Predicates move to data cheaply, precedence does not |
| “How would you test this?” | A seeded generator plus hand-constructed Hand objects, exactly like Decision 1 soft versus hard and the ace algorithm. The rules engine must be testable without dealing a single card |
9. Cheat sheet
Everything above, reduced to what fits on one card the morning of the interview.
| Ace | hard counts every ace as 1. value is hard + 10 when an ace exists and it fits. Soft exactly when that +10 applied. Bust on hard > 21 |
| Why only one 11 | 11 + 11 = 22 is already a bust, so no 2^a enumeration is ever needed |
| Blackjack | Exactly 2 cards, totalling 21, and not from_split. Pays 3:2 as bet * 3 // 2 in integer cents |
| Bet lives on | Hand, not Player. Split gives independent hands, independent bets, independent settlement |
| Insurance | A separate wager at 2:1 on half the main bet, on a dealer ace, resolved first |
| Dealer | Zero decisions. One DealerPolicy.should_hit. S17 versus H17 is a real variation worth about 0.22 points |
| Player bust | Loses immediately, even if the dealer busts later. This is the house edge |
| Shoe | N decks, cut card at a penetration fraction, pending_shuffle honoured between rounds |
| Counting | Hi-Lo, 20 cards at +1 and 20 at -1 per 52. Detection is the correlation of bet size with true count over a long session |
| Data, not code | Ranks and suits, deck count, penetration, the seed, paytables, counting tags, the installed dealer policy |
| Code, not data | The ace algorithm, the 21 threshold, the blackjack definition, and above all the order of the settlement clauses |
| Do not | Put value on Card, put the bet on Player, make Shoe a Singleton, or reshuffle mid-round |
Back to 08 — Elevator System for the scheduling problem, or 09 — Grocery Store System for the money one.