#!/usr/bin/env python3
"""Monte Carlo: crash game cash-out math, measured.

Models the industry-standard provably-fair crash distribution used by the
major crypto originals (documented publicly in their fairness pages):

    crash point M = (1 - e) / U, U ~ Uniform(0, 1),
    so P(crash point >= x) = (1 - e) / x for any cash-out target x >= 1;
    rounds where U > (1 - e) land below 1.01x and are the instant busts.

We use e = 1% (the edge the biggest originals publish for Crash). This makes
the expected value of a cash-out at ANY target exactly -1% per bet:
    EV(c) = (0.99/c)(c-1) - (1 - 0.99/c) = -0.01.

Measured per auto-cashout target c:
  win rate, mean return per bet (the EV check), and in per-target 1M-round
  streams: the longest losing streak.
Session survival: $100 bankroll, $1 flat bets, play until broke or N rounds.
Martingale: double-after-loss at 2x target, $1 base bet, $100 bankroll.
Time-to-hit: median rounds until >= 10x / 100x / 1000x first appears.

Single run, seed 20260821, every figure reported as measured (no cherry-pick).
"""
import random
import sys
from statistics import median

sys.stdout.reconfigure(encoding="utf-8", errors="replace")

SEED = 20260821
# Optional CLI arg overrides the edge (default 1% = the generic study run).
# `python crash-sim.py 0.03` models Aviator's published 97% RTP.
EDGE = float(sys.argv[1]) if len(sys.argv) > 1 else 0.01
KEEP = 1 - EDGE

def crash_point(rng):
    # Standard provably-fair construction; P(M >= x) = KEEP / x. Rounds below
    # a 1.01x cash-out floor are the instant busts (probability ~2%).
    return KEEP / rng.random()

# ---------------------------------------------------------------- EV per target
TARGETS = [1.01, 1.1, 1.5, 2, 3, 5, 10, 20, 50, 100, 1000]

print(f"seed {SEED}, edge {EDGE:.0%}")
print(f"{'target':>8} {'rounds':>12} {'win rate':>9} {'theory':>8} {'EV/bet':>8} {'longest lose streak':>20}")
total_rounds = 0
for c in TARGETS:
    # rare targets need more rounds for the EV estimate to be worth printing
    n = 20_000_000 if c >= 100 else 2_000_000
    rng = random.Random(SEED + int(c * 100))
    wins = 0
    ret = 0.0
    streak = worst = 0
    for _ in range(n):
        m = crash_point(rng)
        if m >= c:
            wins += 1
            ret += c - 1
            streak = 0
        else:
            ret -= 1
            streak += 1
            worst = max(worst, streak)
    total_rounds += n
    print(f"{c:>8} {n:>12,} {wins/n:>9.4%} {KEEP/c:>8.4%} {ret/n:>+8.4%} {worst:>20}")

# ---------------------------------------------------------------- survival
print("\n$100 bankroll, $1 flat bets, cap 2,000 rounds — 50,000 sessions per target")
N_SESS = 50_000
CAP = 2_000
print(f"{'target':>8} {'busted<cap':>10} {'median rounds (busted only)':>28} {'alive@500':>10} {'alive@2000':>11} {'ever 2x bank':>13}")
for c in [1.5, 2, 5, 10, 100]:
    rng = random.Random(SEED * 7 + int(c * 100))
    busted_rounds, doubled, busted = [], 0, 0
    alive500 = alive_cap = 0
    for _ in range(N_SESS):
        bal, peak_hit, r = 100.0, False, 0
        while bal >= 1 and r < CAP:
            r += 1
            m = crash_point(rng)
            bal += (c - 1) if m >= c else -1
            if bal >= 200:
                peak_hit = True
        if bal < 1:
            busted += 1
            busted_rounds.append(r)
        doubled += peak_hit
        alive500 += r >= 500  # sessions only end by bust or cap, so this = survived 500 rounds
        alive_cap += bal >= 1 and r >= CAP
        total_rounds += r
    med = median(busted_rounds) if busted_rounds else float('nan')
    print(f"{c:>8} {busted/N_SESS:>10.1%} {med:>28} {alive500/N_SESS:>10.1%} {alive_cap/N_SESS:>11.1%} {doubled/N_SESS:>13.2%}")

# ---------------------------------------------------------------- martingale
print("\nMartingale at 2x target: $1 base, double after loss, $100 bankroll, 500-round sessions x 100,000")
rng = random.Random(SEED * 13)
busts = 0
end_bal = []
for _ in range(100_000):
    bal, bet = 100.0, 1.0
    for r in range(500):
        if bet > bal:
            busts += 1
            break
        m = crash_point(rng)
        if m >= 2:
            bal += bet  # 2x pays 2*bet on a bet staked: net +bet
            bet = 1.0
        else:
            bal -= bet
            bet *= 2
        total_rounds += 1
    else:
        end_bal.append(bal)
print(f"  wiped out (cannot cover next doubled bet): {busts/100_000:.2%}")
if end_bal:
    print(f"  finishers: {len(end_bal)/100_000:.2%}, median end balance ${median(end_bal):.2f}")

# ---------------------------------------------------------------- time to hit
print("\nrounds until a multiplier first appears (10,000 trials each)")
for x in [10, 100, 1000]:
    rng = random.Random(SEED * 17 + x)
    waits = []
    for _ in range(10_000):
        r = 0
        while True:
            r += 1
            if crash_point(rng) >= x:
                break
            if r > 3_000_000:
                break
        waits.append(r)
        total_rounds += r
    print(f"  >= {x:>5}x: median {median(waits):,} rounds, mean {sum(waits)/len(waits):,.0f}")

print(f"\ntotal simulated rounds: {total_rounds:,}")
