Back in Station 4 you played blackjack with a hand-wavy rule ("hit until 17"). Real players memorise a basic-strategy chart — a grid telling you hit/stand/double for every hand. But where does that chart come from? It isn't guessed or tuned. It's calculated — the mathematically perfect play, provably. In this challenge you'll compute it yourself, from first principles, and never trust a memorised chart again.
The tool is dynamic programming: break a hard question ("what's the best play here?") into the same question about simpler positions, solve those, and build up. It's the workhorse behind optimal decision-making everywhere — from blackjack to option pricing to routing to reinforcement learning.
Three layers, each built on the last:
| 1. Dealer | The dealer's play is fixed (hit below 17, stand on 17+). Compute the exact probability of each final total (17, 18, 19, 20, 21, bust) for every upcard — by recursion. |
| 2. Standing | Given that dealer distribution, the EV of standing on any total is just arithmetic: sum up win / lose / push. |
| 3. Best play | The EV of hitting is the average over the next card of the best EV of the new hand — a recursion into simpler hands. Double is "one card then stand, at double stake." Take the max: that's optimal play, and the argmax is your chart. |
from functools import lru_cache # Infinite-deck blackjack, dealer stands on all 17. # Each draw: 2..9 with prob 1/13 each, a ten-value card 4/13, an Ace 1/13. DRAW = [(v, 1/13) for v in range(2, 10)] + [(10, 4/13), ('A', 1/13)] def add_card(total, soft, card): """Add a card. `soft` = how many aces are still counted as 11 (Station 4!).""" if card == 'A': total += 11; soft += 1 else: total += card while total > 21 and soft > 0: # an ace drops from 11 to 1 to save you total -= 10; soft -= 1 return total, soft def dealer_dist(total, soft): """Probabilities of the dealer's FINAL total (17..21) or 'bust', starting from (total, soft). Dealer hits below 17, stands on 17+. Return a dict like {17:.., 18:.., ..., 'bust':..}. MEMOISE this.""" # TODO: if total > 21 -> {'bust':1.0}; if total >= 17 -> {total:1.0} # TODO: else draw one card, recurse, and sum the weighted sub-distributions ... def stand_ev(player_total, dealer): """EV of standing on player_total vs the dealer distribution `dealer`.""" # TODO: for each dealer outcome k with prob p: # win +1 if k=='bust' or player_total > k; push 0 if equal; else lose -1 ... def best_ev(total, soft, dealer, can_double): """Best EV over the legal actions.""" if total > 21: return -1 st = stand_ev(total, dealer) # hit: average over the next card of best_ev of the new hand (no more doubling) hit = sum(p * best_ev(*add_card(total, soft, c), dealer, False) for c, p in DRAW) best = max(st, hit) if can_double: # TODO: double = take exactly ONE card then stand, at 2x stake # (a bust while doubling loses 2). best = max(best, that) ... return best # dealer distribution for a single upcard u: dealer_dist(*add_card(0, 0, u))
Memoise the dealer. dealer_dist is called a huge number of times with the same arguments — wrap it with @lru_cache (return a tuple of items, or freeze the dict) or keep your own memo dictionary. Without it, it's painfully slow.
The hit recursion always terminates because every card makes the total bigger (or an ace resolves); eventually you stand or bust. Busting returns −1.
Double is the easy one: draw one card, and if you didn't bust, stand — but everything is worth twice as much (and a bust costs −2).
Reading the chart: for each dealer upcard, build its distribution once, then for each player total call best_ev and also check which action won (compare the stand/hit/double EVs). That argmax is the basic-strategy chart.
If your dealer bust rate for a 6 comes out around 0.42, your dealer layer is right — that's the foundation everything else stands on.
Build the DP, then compute these. The first two prove your dealer layer; the third proves the player layer and nails the most famous decision in blackjack.
You just solved a decision problem by working backwards from the end — the essence of dynamic programming and, one step further, reinforcement learning. The same skeleton (states, actions, expected values, take the max) prices American options, plans routes, and underlies how agents learn to play games. Want the next rung? Ask Smee about the market-making challenge.