"""Blackjack: basic strategy charts (4-8 decks) and a Monte Carlo house-edge estimate per rule set. Rules simulated: 6 decks, reshuffle at 75% penetration, dealer peeks for blackjack, double on any first two cards, one split per hand (no resplitting), split aces receive one card each, no surrender. Strategy charts follow the Wizard of Odds 4-8 deck basic strategy (cited on the site). Payout variants (3:2 vs 6:5) are scored on the same hands, so the difference is exact for this sample. """ import random, json, os, sys from multiprocessing import Pool from common import save, SIMS UP = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # 11 = ace def chart(h17: bool, das: bool): hard, soft, pair = {}, {}, {} for t in range(4, 22): row = {} for u in UP: if t <= 8: a = "H" elif t == 9: a = "D" if 3 <= u <= 6 else "H" elif t == 10: a = "D" if u <= 9 else "H" elif t == 11: a = "D" if (u <= 10 or h17) else "H" elif t == 12: a = "S" if 4 <= u <= 6 else "H" elif t <= 16: a = "S" if u <= 6 else "H" else: a = "S" row[u] = a hard[t] = row for o in range(2, 10): # soft totals A+o = 13..20 row = {} for u in UP: if o in (2, 3): a = "D" if u in (5, 6) else "H" elif o in (4, 5): a = "D" if 4 <= u <= 6 else "H" elif o == 6: a = "D" if 3 <= u <= 6 else "H" elif o == 7: if 3 <= u <= 6: a = "Ds" elif u == 2: a = "Ds" if h17 else "S" elif u in (7, 8): a = "S" else: a = "H" elif o == 8: a = "Ds" if (h17 and u == 6) else "S" else: a = "S" row[u] = a soft[11 + o] = row for c in range(2, 12): row = {} for u in UP: if c in (2, 3): a = "P" if (2 <= u <= 7 if das else 4 <= u <= 7) else "H" elif c == 4: a = "P" if (das and u in (5, 6)) else "H" elif c == 5: a = hard[10][u] elif c == 6: a = "P" if (2 <= u <= 6 if das else 3 <= u <= 6) else "H" elif c == 7: a = "P" if u <= 7 else "H" elif c == 8: a = "P" elif c == 9: a = "S" if u in (7, 10, 11) else "P" elif c == 10: a = "S" else: a = "P" row[u] = a pair[c] = row return {"hard": hard, "soft": soft, "pair": pair} def total(cards): t = sum(cards); aces = cards.count(11) while t > 21 and aces: t -= 10; aces -= 1 return t, aces > 0 class Shoe: def __init__(self, rng, decks=6): self.rng = rng; self.decks = decks; self.shuffle() def shuffle(self): self.cards = ([2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11] * 4) * self.decks self.rng.shuffle(self.cards); self.pos = 0 def draw(self): c = self.cards[self.pos]; self.pos += 1; return c def maybe_shuffle(self): if self.pos > len(self.cards) * 0.75: self.shuffle() def dealer_play(cards, shoe, h17): while True: t, soft = total(cards) if t < 17 or (h17 and t == 17 and soft): cards.append(shoe.draw()) else: return t def play_hand(cards, up, shoe, ch, can_double, can_split, split_aces=False): """Returns list of (cards, bet_multiplier). Bet multiplier 2 for doubles.""" if split_aces: cards.append(shoe.draw()); return [(cards, 1)] if can_split and len(cards) == 2 and cards[0] == cards[1]: a = ch["pair"][cards[0]][up] if a == "P": aces = cards[0] == 11 out = [] for c in (cards[0], cards[1]): h = [c, shoe.draw()] out += play_hand(h, up, shoe, ch, ch["das"], False, False) if not aces else [(h, 1)] return out first = True while True: t, soft = total(cards) if t > 21: return [(cards, 1)] if t >= 21: return [(cards, 1)] if soft and t >= 13 and t <= 20: a = ch["soft"][t][up] else: a = ch["hard"][max(4, t)][up] if a in ("D", "Ds"): if first and can_double: cards.append(shoe.draw()); return [(cards, 2)] a = "S" if a == "Ds" else "H" if a == "S": return [(cards, 1)] cards.append(shoe.draw()); first = False def run(args): h17, das, n, seed = args rng = random.Random(seed) shoe = Shoe(rng) ch = chart(h17, das); ch["das"] = das win32 = win65 = 0.0; bjs = 0 for _ in range(n): shoe.maybe_shuffle() p = [shoe.draw(), shoe.draw()]; d = [shoe.draw(), shoe.draw()] up = d[0] pbj = total(p)[0] == 21; dbj = total(d)[0] == 21 if pbj or dbj: if pbj and not dbj: win32 += 1.5; win65 += 1.2; bjs += 1 elif dbj and not pbj: win32 -= 1; win65 -= 1 continue hands = play_hand(p, up, shoe, ch, True, True) need_dealer = any(total(h)[0] <= 21 for h, _ in hands) dt = dealer_play(d, shoe, h17) if need_dealer else 0 res = 0.0 for h, m in hands: t = total(h)[0] if t > 21: res -= m elif dt > 21 or t > dt: res += m elif t < dt: res -= m win32 += res; win65 += res return win32, win65, bjs, n if __name__ == "__main__": SEED = 20261004 N = int(sys.argv[1]) if len(sys.argv) > 1 else 2_000_000 results = [] with Pool(2) as pool: for h17 in (False, True): for das in (True, False): parts = pool.map(run, [(h17, das, N // 2, SEED + i + 10 * h17 + 100 * das) for i in range(2)]) w32 = sum(p[0] for p in parts); w65 = sum(p[1] for p in parts); bj = sum(p[2] for p in parts); n = sum(p[3] for p in parts) se = 1.15 / n ** 0.5 * 100 results.append({"rules": f"6 decks, {'H17' if h17 else 'S17'}, {'DAS' if das else 'no DAS'}", "hands": n, "edge_3to2_pct": round(-w32 / n * 100, 3), "edge_6to5_pct": round(-w65 / n * 100, 3), "blackjack_freq_pct": round(bj / n * 100, 3), "std_error_pct": round(se, 3)}) print(results[-1], flush=True) charts = {} for h17 in (False, True): for das in (True, False): c = chart(h17, das) charts[f"{'h17' if h17 else 's17'}_{'das' if das else 'nodas'}"] = {k: {str(t): {str(u): a for u, a in row.items()} for t, row in v.items()} for k, v in c.items()} save("blackjack", {"game": "Blackjack", "hands_per_rule_set": N, "seed": SEED, "rules": "6 decks, 75% penetration, dealer peeks, double any two, one split, split aces one card, no surrender", "note": "Monte Carlo estimate; compare with exact figures from the Wizard of Odds calculator.", "script": "blackjack_ev.py"}, {"results": results, "charts": charts}, [[r["rules"], r["hands"], r["edge_3to2_pct"], r["edge_6to5_pct"], r["std_error_pct"]] for r in results], ["rules", "hands", "house_edge_3to2_pct", "house_edge_6to5_pct", "std_error_pct"])