"""
ai-casino.online extra simulations, October 2026.
Reproducible: run `python3 extra_sims.py` (needs numpy). Fixed seed 20261004.
Writes extra-summary.json next to where it is run.

1) Blackjack dealer bust rate by up card (infinite deck, dealer stands on soft 17)
2) Hard 16 (10,6) v dealer 10: stand v hit, 2,000,000 hands each
3) Hi-Lo true count frequency in a 6-deck shoe, 75% penetration
4) Baccarat streaks: 20,000 eight-deck shoes played to the cut card
5) Crash games: 1,000,000 rounds at 97% RTP, plus "predictor" hit rates
6) Live roulette tracker: 100,000 sessions of 100 spins on a European wheel
7) A UK bonus at the 10x wagering cap: GBP 20 bonus, 200 spins at GBP 1 on a 96% slot
"""
import numpy as np, json
rng = np.random.default_rng(20261004)
out = {}
# card ranks 1..10, ace = 1, tens weighted 4/13
RANKS = np.array([1,2,3,4,5,6,7,8,9,10])
PROBS = np.array([1,1,1,1,1,1,1,1,1,4])/13

def draw(n, k):
    return rng.choice(RANKS, size=(n, k), p=PROBS)

def dealer_finish(up, n):
    """Return dealer final totals (22 = bust) for n hands from a given up card, S17. Blackjack counted as 21."""
    cards = draw(n, 15)
    total = np.full(n, up); aces = (np.full(n, up) == 1).astype(int)
    done = np.zeros(n, bool)
    for i in range(15):
        c = cards[:, i]
        total = np.where(done, total, total + c)
        aces = np.where(done, aces, aces + (c == 1))
        soft = (aces > 0) & (total + 10 <= 21)
        best = np.where(soft, total + 10, total)
        done = done | (best >= 17)
        if done.all(): break
    soft = (aces > 0) & (total + 10 <= 21)
    best = np.where(soft, total + 10, total)
    return np.where(best > 21, 22, best)

# 1) bust by up card
N = 2_000_000
bust = {}
for up in range(1, 11):
    f = dealer_finish(up, N)
    bust['A' if up == 1 else str(up)] = round(float((f == 22).mean()) * 100, 1)
out['dealer_bust_pct'] = bust

# 2) 16 v 10
N = 2_000_000
d = dealer_finish(10, N)
stand_win = (d == 22)
stand_loss = ~stand_win  # 16 never beats a made dealer hand
# hit: draw until hard/soft total >= 17 (correct follow-up v a 10)
cards = draw(N, 10)
tot = np.full(N, 16); aces = np.zeros(N, int); stop = np.zeros(N, bool)
for i in range(10):
    c = cards[:, i]
    tot = np.where(stop, tot, tot + c)
    aces = np.where(stop, aces, aces + (c == 1))
    best = np.where((aces > 0) & (tot + 10 <= 21), tot + 10, tot)
    stop = stop | (best >= 17)
best = np.where((aces > 0) & (tot + 10 <= 21), tot + 10, tot)
d2 = dealer_finish(10, N)
p_bust = best > 21
win = (~p_bust) & ((d2 == 22) | (best > d2))
push = (~p_bust) & (d2 != 22) & (best == d2)
loss = ~(win | push)
out['hard16_v10'] = dict(hands=N,
    stand_loss_pct=round(float(stand_loss.mean())*100, 1), stand_ev=round(float(stand_win.mean() - stand_loss.mean()), 3),
    hit_loss_pct=round(float(loss.mean())*100, 1), hit_push_pct=round(float(push.mean())*100, 1),
    hit_bust_pct=round(float(p_bust.mean())*100, 1), hit_ev=round(float(win.mean() - loss.mean()), 3))

# 3) Hi-Lo true count frequency
SHOES = 100_000; DECKS = 6; CARDS = 52*DECKS; PEN = int(CARDS*0.75)
deck = np.array(([2,3,4,5,6,7,8,9,10,10,10,10,11])*4*DECKS)
hilo = np.where(deck <= 6, 1, np.where(deck >= 10, -1, 0))
tcs = []
for _ in range(SHOES):
    v = rng.permutation(hilo)[:PEN]
    rc = np.cumsum(v)
    idx = np.arange(5, PEN, 5)        # a new round roughly every 5 cards (heads-up)
    decks_left = (CARDS - idx) / 52
    tcs.append(np.floor(rc[idx-1] / decks_left))
tcs = np.concatenate(tcs)
out['hilo_true_count'] = dict(shoes=SHOES, decks=DECKS, penetration_pct=75, rounds=int(tcs.size),
    tc_ge_1_pct=round(float((tcs >= 1).mean())*100, 1), tc_ge_2_pct=round(float((tcs >= 2).mean())*100, 1),
    tc_ge_3_pct=round(float((tcs >= 3).mean())*100, 1), tc_le_minus1_pct=round(float((tcs <= -1).mean())*100, 1))

# 4) Baccarat streaks
def bacc_val(c): return 0 if c >= 10 else c
def play_shoe():
    shoe = rng.permutation(np.repeat(np.arange(1, 14), 4*8))
    cut = len(shoe) - 14; i = 0; res = []
    while i + 6 <= cut:
        p = [shoe[i], shoe[i+2]]; b = [shoe[i+1], shoe[i+3]]; i += 4
        pt = (bacc_val(p[0]) + bacc_val(p[1])) % 10; bt = (bacc_val(b[0]) + bacc_val(b[1])) % 10
        if pt < 8 and bt < 8:
            p3 = None
            if pt <= 5:
                p3 = bacc_val(shoe[i]); i += 1; pt = (pt + p3) % 10
            if p3 is None:
                if bt <= 5: bt = (bt + bacc_val(shoe[i])) % 10; i += 1
            else:
                draw_b = (bt <= 2) or (bt == 3 and p3 != 8) or (bt == 4 and 2 <= p3 <= 7) or \
                         (bt == 5 and 4 <= p3 <= 7) or (bt == 6 and p3 in (6, 7))
                if draw_b: bt = (bt + bacc_val(shoe[i])) % 10; i += 1
        res.append('B' if bt > pt else ('P' if pt > bt else 'T'))
    return res
SH = 20_000
longest_b = []; long6 = 0; hands = 0; after4 = [0, 0]; counts = {'B': 0, 'P': 0, 'T': 0}
for _ in range(SH):
    r = play_shoe(); hands += len(r)
    for x in r: counts[x] += 1
    seq = [x for x in r if x != 'T']
    best = cur = 0; prev = None; best_any = 0
    for j, x in enumerate(seq):
        cur = cur + 1 if x == prev else 1; prev = x
        if x == 'B': best = max(best, cur)
        best_any = max(best_any, cur)
        if j >= 4 and seq[j-4:j] == ['B']*4:
            after4[0] += 1; after4[1] += (x == 'B')
    longest_b.append(best); long6 += best_any >= 6
out['baccarat'] = dict(shoes=SH, hands=hands, hands_per_shoe=round(hands/SH, 1),
    banker_pct=round(counts['B']/hands*100, 2), player_pct=round(counts['P']/hands*100, 2), tie_pct=round(counts['T']/hands*100, 2),
    avg_longest_banker_run=round(float(np.mean(longest_b)), 1),
    shoes_with_run_6_plus_pct=round(long6/SH*100, 1),
    banker_after_4_bankers_pct=round(after4[1]/after4[0]*100, 2), times_4_bankers=after4[0])

# 5) Crash games, 97% RTP: P(crash >= x) = 0.97/x
R = 1_000_000
u = rng.random(R)
crash = np.maximum(1.0, np.floor(0.97/u*100)/100)
sess = crash[:R//1000*1000].reshape(-1, 1000)
def longest_under(s, x):
    m = 0
    for row in s:
        c = b = 0
        for v in row < x:
            c = c+1 if v else 0; b = max(b, c)
        m += b
    return m/len(s)
out['crash'] = dict(rounds=R, rtp=97,
    reach_1_5x_pct=round(float((crash >= 1.5).mean())*100, 1), reach_2x_pct=round(float((crash >= 2).mean())*100, 1),
    reach_10x_pct=round(float((crash >= 10).mean())*100, 2), reach_100x_pct=round(float((crash >= 100).mean())*100, 3),
    instant_bust_pct=round(float((crash < 1.01).mean())*100, 1), median_crash=float(np.median(crash)),
    avg_longest_run_under_2x_per_1000=round(longest_under(sess[:300], 2), 1))
# "predictor" that always calls 'over 1.5x': chance of 8 or more right out of 10
from math import comb
p = 0.97/1.5
out['crash']['predictor_8of10_over_1_5x_pct'] = round(sum(comb(10,k)*p**k*(1-p)**(10-k) for k in range(8,11))*100, 1)

# 6) Live roulette tracker, 100-spin sessions
S = 100_000
spins = rng.integers(0, 37, size=(S, 100))
cnt = np.apply_along_axis(lambda r: np.bincount(r, minlength=37), 1, spins)
out['tracker_100'] = dict(sessions=S, spins=100,
    some_number_6plus_pct=round(float((cnt.max(axis=1) >= 6).mean())*100, 1),
    some_number_7plus_pct=round(float((cnt.max(axis=1) >= 7).mean())*100, 1),
    avg_numbers_never_hit=round(float((cnt == 0).sum(axis=1).mean()), 2),
    avg_hottest_count=round(float(cnt.max(axis=1).mean()), 2),
    hot_next_spin_pct=None)
# does the hottest number of the first 100 spins hit more on the next 100?
nxt = rng.integers(0, 37, size=(S, 100))
hot = cnt.argmax(axis=1)
out['tracker_100']['hot_hits_next_100_avg'] = round(float((nxt == hot[:, None]).sum(axis=1).mean()), 3)
out['tracker_100']['expected_hits_per_100'] = round(100/37, 3)

# 7) GBP 20 bonus at the 10x cap: GBP 200 wagering, GBP 1 spins on the medium-volatility 96% slot from simulate.py
pays = np.array([0, 0.5, 2, 5, 20, 100.0]); probs = [0.70, 0.12, 0.08, 0.04, 0.015, None]
known = sum(a*b for a, b in zip(pays[:-1], probs[:-1])); probs[-1] = (0.96 - known)/pays[-1]; probs[0] = 1 - sum(probs[1:])
probs = np.array(probs)
S = 100_000
draws = pays[rng.choice(6, size=(S, 200), p=probs)]
bal = np.full(S, 20.0); alive = np.ones(S, bool); wag = np.zeros(S)
for i in range(200):
    can = alive & (bal >= 1)
    bal = np.where(can, bal - 1 + draws[:, i], bal); wag += can
    alive = can
done = wag >= 200
out['bonus_10x'] = dict(sessions=S, bonus=20, wagering=200, stake=1, rtp=96,
    completed_pct=round(float(done.mean())*100, 1),
    avg_cash_after_completing=round(float(bal[done].mean()), 2),
    median_cash_all=round(float(np.median(np.where(done, bal, 0))), 2),
    avg_cash_all=round(float(np.where(done, bal, 0).mean()), 2),
    expected_cost_theory=round(200*0.04, 2))
json.dump(out, open('extra-summary.json', 'w'), indent=1)
print(json.dumps(out, indent=1))
