#!/usr/bin/env python3
"""Blackjack Monte Carlo — 10M hands total.

Rules: 8 decks, dealer stands on soft 17 (S17), blackjack pays 3:2,
double on any two cards, double after split allowed (DAS),
resplit up to 4 hands, split aces get one card each (no resplit, no BJ),
no surrender, no insurance, dealer peeks for blackjack (US holecard).
Shoe reshuffled when fewer than 52 cards remain.

Strategies:
  basic      — total-dependent basic strategy for the above rules (8,000,000 hands)
  mimic      — mimic the dealer: hit to hard 17+/soft 17 stands, never double/split (1,000,000)
  neverbust  — stand on any hard 12+, hit soft below 18, never double/split (1,000,000)

Seed: 20260716
"""
import random
from collections import Counter

random.seed(20260716)

DECKS = 8
# ranks: 1 = ace, 2-9, 10 (includes J/Q/K)
BASE_SHOE = ([1,2,3,4,5,6,7,8,9] * 4 + [10] * 16) * DECKS  # 416 cards

class Shoe:
    def __init__(self):
        self.cards = []
        self.pos = 0
    def draw(self):
        if self.pos >= len(self.cards) - 0:
            pass
        return self.cards.pop()
    def ensure(self):
        if len(self.cards) < 52:
            self.cards = BASE_SHOE[:]
            random.shuffle(self.cards)

def hand_value(cards):
    """Return (total, is_soft)."""
    total = sum(cards)
    soft = False
    if 1 in cards and total + 10 <= 21:
        total += 10
        soft = True
    return total, soft

# ---------- basic strategy (8 deck, S17, DAS) ----------

def bs_pair_split(rank, up):
    if rank == 1: return True
    if rank == 8: return True
    if rank == 9: return up in (2,3,4,5,6,8,9)
    if rank == 7: return up in (2,3,4,5,6,7)
    if rank == 6: return up in (2,3,4,5,6)      # DAS: includes 2
    if rank == 4: return up in (5,6)             # DAS
    if rank in (2,3): return up in (2,3,4,5,6,7) # DAS: includes 2,3
    return False  # 5s and 10s never split

def bs_action(cards, up, can_double, can_split):
    """Return 'H','S','D','P'. up: 1=ace, 2-10."""
    if can_split and len(cards) == 2 and cards[0] == cards[1]:
        if bs_pair_split(cards[0], up):
            return 'P'
    total, soft = hand_value(cards)
    two = len(cards) == 2
    if soft:
        # soft totals (A counted as 11)
        if total >= 19: return 'S'
        if total == 18:
            if up in (3,4,5,6) and can_double and two: return 'D'
            if up in (2,7,8): return 'S'
            return 'H'
        if total == 17:
            return 'D' if up in (3,4,5,6) and can_double and two else 'H'
        if total in (15,16):
            return 'D' if up in (4,5,6) and can_double and two else 'H'
        # soft 13,14
        return 'D' if up in (5,6) and can_double and two else 'H'
    # hard totals
    if total >= 17: return 'S'
    if total >= 13: return 'S' if up in (2,3,4,5,6) else 'H'
    if total == 12: return 'S' if up in (4,5,6) else 'H'
    if total == 11:
        return 'D' if up != 1 and can_double and two else 'H'
    if total == 10:
        return 'D' if up in (2,3,4,5,6,7,8,9) and can_double and two else 'H'
    if total == 9:
        return 'D' if up in (3,4,5,6) and can_double and two else 'H'
    return 'H'

def mimic_action(cards, up, can_double, can_split):
    total, soft = hand_value(cards)
    return 'S' if total >= 17 else 'H'

def neverbust_action(cards, up, can_double, can_split):
    total, soft = hand_value(cards)
    if soft:
        return 'S' if total >= 18 else 'H'
    return 'S' if total >= 12 else 'H'

# ---------- game engine ----------

def play_dealer(shoe, cards):
    while True:
        total, soft = hand_value(cards)
        if total >= 17:  # S17: stand on all 17s including soft
            return total
        cards.append(shoe.draw())

def simulate(n_hands, action_fn, allow_split_double=True, label=""):
    shoe = Shoe()
    stats = Counter()
    total_initial = 0.0   # initial bets (1 per hand)
    total_wagered = 0.0   # including doubles and splits
    net = 0.0
    for _ in range(n_hands):
        shoe.ensure()
        d = shoe.draw
        player = [d(), d()]
        dealer = [d(), d()]
        up = dealer[0]
        total_initial += 1.0
        wager_this = 1.0

        dv, _ = hand_value(dealer)
        dealer_bj = (dv == 21)
        pv, _ = hand_value(player)
        player_bj = (pv == 21)

        # dealer peeks with A or 10 up
        if dealer_bj:
            stats['dealer_bj'] += 1
            if player_bj:
                stats['push'] += 1
            else:
                stats['lose'] += 1
                net -= 1.0
            total_wagered += wager_this
            continue
        if player_bj:
            stats['player_bj'] += 1
            stats['win'] += 1
            net += 1.5
            total_wagered += wager_this
            continue

        # play out player hand(s)
        hands = [(player, 1.0, False)]  # (cards, bet, from_split_aces)
        final = []  # (total_or_None-if-bust, bet)
        n_splits = 0
        while hands:
            cards, bet, split_aces = hands.pop()
            if split_aces:
                final.append((hand_value(cards)[0], bet))
                continue
            while True:
                can_split = (allow_split_double and len(cards) == 2
                             and cards[0] == cards[1] and n_splits < 3
                             and not (cards[0] == 1 and n_splits >= 1))
                can_double = allow_split_double
                act = action_fn(cards, up, can_double, can_split)
                if act == 'P':
                    n_splits += 1
                    stats['splits'] += 1
                    c = cards[0]
                    if c == 1:
                        h1 = [c, shoe.draw()]; h2 = [c, shoe.draw()]
                        hands.append((h1, bet, True))
                        hands.append((h2, bet, True))
                        wager_this += bet
                        cards = None
                        break
                    hands.append(([c, shoe.draw()], bet, False))
                    cards = [c, shoe.draw()]
                    wager_this += bet
                    continue
                if act == 'D':
                    stats['doubles'] += 1
                    bet *= 2
                    wager_this += bet / 2
                    cards.append(shoe.draw())
                    t, _s = hand_value(cards)
                    final.append((t if t <= 21 else None, bet))
                    cards = None
                    break
                if act == 'H':
                    cards.append(shoe.draw())
                    t, _s = hand_value(cards)
                    if t > 21:
                        final.append((None, bet))
                        cards = None
                        break
                    continue
                # 'S'
                final.append((hand_value(cards)[0], bet))
                cards = None
                break

        total_wagered += wager_this

        # if all player hands busted, dealer doesn't draw
        live = [(t, b) for t, b in final if t is not None]
        for t, b in final:
            if t is None:
                stats['player_bust_hands'] += 1
                net -= b
                stats['lose'] += 1
        if not live:
            continue
        dt = play_dealer(shoe, dealer)
        if dt > 21:
            stats['dealer_bust'] += 1
            for t, b in live:
                net += b
                stats['win'] += 1
        else:
            for t, b in live:
                if t > dt:
                    net += b; stats['win'] += 1
                elif t < dt:
                    net -= b; stats['lose'] += 1
                else:
                    stats['push'] += 1

    print(f"=== {label} ({n_hands:,} hands) ===")
    print(f"net: {net:,.1f}  initial: {total_initial:,.0f}  wagered: {total_wagered:,.1f}")
    print(f"house edge (per initial bet): {-net/total_initial*100:.4f}%")
    print(f"element of risk (per unit wagered): {-net/total_wagered*100:.4f}%")
    resolved = stats['win'] + stats['lose'] + stats['push']
    print(f"win: {stats['win']:,} ({stats['win']/resolved*100:.3f}%)  "
          f"lose: {stats['lose']:,} ({stats['lose']/resolved*100:.3f}%)  "
          f"push: {stats['push']:,} ({stats['push']/resolved*100:.3f}%)")
    print(f"player BJ: {stats['player_bj']:,} ({stats['player_bj']/n_hands*100:.3f}%)  "
          f"dealer BJ: {stats['dealer_bj']:,} ({stats['dealer_bj']/n_hands*100:.3f}%)")
    print(f"doubles: {stats['doubles']:,} ({stats['doubles']/n_hands*100:.3f}% of hands)  "
          f"splits: {stats['splits']:,} ({stats['splits']/n_hands*100:.3f}%)")
    ndealt = n_hands - stats['dealer_bj'] - stats['player_bj']
    if stats['dealer_bust']:
        print(f"dealer bust (when dealer drew): {stats['dealer_bust']:,}")
    print(f"player bust hands: {stats['player_bust_hands']:,}")
    print()

simulate(8_000_000, bs_action, True, "BASIC STRATEGY")
simulate(1_000_000, mimic_action, False, "MIMIC THE DEALER")
simulate(1_000_000, neverbust_action, False, "NEVER BUST")
