#!/usr/bin/env python3
"""Monte Carlo: what actually happens when you claim a casino welcome bonus.

Question nobody publishes an answer to: if you take a 100% match with a 40x
wagering requirement, how often do you finish the requirement at all, and what
is your balance when the dust settles?

Model, stated in full so it is reproducible:
  deposit D, bonus B = D (100% match)
  wagering requirement W = mult * (D + B)      [bonus+deposit basis, the common case]
  flat bet size b, play until either:
      cumulative wagered >= W        -> "cleared", withdraw whole balance
      balance < b                    -> "busted", withdraw nothing
  Bonus funds are forfeited on bust; on clearing, balance is cash.

Two game models bracket the realistic range:
  LOW-VOL : even-money bet, win prob p = (1 - edge)/2 -> exact house edge
  SLOT    : discrete multiplier table, RTP verified exactly in code

Seed 20260802.
"""
import random
from collections import Counter

random.seed(20260802)

# ---------- game models ----------

def make_slot(rtp_target):
    """Medium-volatility slot: multipliers and probabilities published below.
    The 0x bucket absorbs the remainder so the RTP lands exactly on target."""
    table = [(0.5, 0.180), (1.0, 0.090), (2.0, 0.055),
             (5.0, 0.022), (20.0, 0.0065), (100.0, 0.0012)]
    paid = sum(m * p for m, p in table)
    # scale the paying buckets so total RTP == rtp_target
    scale = rtp_target / paid
    scaled = [(m, p * scale) for m, p in table]
    p_pay = sum(p for _, p in scaled)
    assert p_pay < 1, "paying probability exceeded 1"
    dist = [(0.0, 1 - p_pay)] + scaled
    rtp = sum(m * p for m, p in dist)
    assert abs(rtp - rtp_target) < 1e-12
    return dist

def spin(dist, r):
    acc = 0.0
    for m, p in dist:
        acc += p
        if r < acc:
            return m
    return dist[-1][0]

# ---------- simulation ----------

def run(n, deposit, mult, bet, game, edge=None, dist=None, max_spins=2_000_000):
    W = mult * (deposit * 2)          # 100% match -> wager on deposit+bonus
    cleared = 0
    final_balances = []
    wagered_total = 0.0
    spins_total = 0
    p_win = (1 - edge) / 2 if edge is not None else None
    for _ in range(n):
        bal = deposit * 2.0
        wagered = 0.0
        spins = 0
        while wagered < W and bal >= bet and spins < max_spins:
            r = random.random()
            if game == 'even':
                bal += bet if r < p_win else -bet
            else:
                bal += spin(dist, r) * bet - bet
            wagered += bet
            spins += 1
        spins_total += spins
        wagered_total += wagered
        if wagered >= W and bal >= 0:
            cleared += 1
            final_balances.append(bal)
        else:
            final_balances.append(0.0)
    avg_final = sum(final_balances) / n
    return {
        'clear_rate': cleared / n,
        'avg_final': avg_final,
        'net_vs_deposit': avg_final - deposit,
        'avg_wagered': wagered_total / n,
        'avg_spins': spins_total / n,
        'requirement': W,
    }

def show(label, r, deposit):
    print(f"--- {label}")
    print(f"    wagering requirement      : ${r['requirement']:,.0f}")
    print(f"    cleared the requirement   : {r['clear_rate']*100:.2f}%")
    print(f"    avg wagered before ending : ${r['avg_wagered']:,.0f}  ({r['avg_spins']:,.0f} bets)")
    print(f"    avg final balance         : ${r['avg_final']:.2f}")
    print(f"    net vs the ${deposit} deposited  : ${r['net_vs_deposit']:+.2f}"
          f"  ({r['net_vs_deposit']/deposit*100:+.1f}%)")
    print()

N = 200_000
DEPOSIT = 100
BET = 1.0

print(f"=== 100% match, ${DEPOSIT} deposit, flat ${BET:.0f} bets, {N:,} simulated bonus claims each ===\n")

slot96 = make_slot(0.96)
print("slot model (RTP 96.00% exactly):")
for m, p in slot96:
    print(f"    x{m:<6g} p={p:.6f}")
print()

for mult in (20, 30, 40):
    show(f"SLOT  RTP 96%   |  {mult}x wagering", run(N, DEPOSIT, mult, BET, 'slot', dist=slot96), DEPOSIT)

for mult in (20, 30, 40):
    show(f"EVEN  edge 1.0% |  {mult}x wagering", run(N, DEPOSIT, mult, BET, 'even', edge=0.010), DEPOSIT)

# no-deposit style: bonus only, no player money at risk
print("=== $20 no-deposit bonus, 40x on the bonus, flat $0.50 bets ===\n")
def run_ndb(n, bonus, mult, bet, dist, cap=None):
    W = mult * bonus
    cleared = 0; finals = []
    for _ in range(n):
        bal = float(bonus); wagered = 0.0
        while wagered < W and bal >= bet:
            bal += spin(dist, random.random()) * bet - bet
            wagered += bet
        if wagered >= W:
            cleared += 1
            finals.append(min(bal, cap) if cap else bal)
        else:
            finals.append(0.0)
    return cleared / n, sum(finals) / n
cr, avg = run_ndb(100_000, 20, 40, 0.5, slot96, cap=100)
print(f"    cleared 40x ($800 wagered) : {cr*100:.2f}%")
print(f"    avg payout (capped at $100): ${avg:.2f}")
