"""
ai-casino.online: Mines odds research (October 2026)

Reproduces every figure on https://www.ai-casino.online/crash-games-ai/mines/
and in the Mines section of https://www.ai-casino.online/casino-simulations/

1. Exact odds: chance of clearing k safe tiles on a 5x5 grid with m mines,
   and the multiplier a game paying a given RTP must offer.
2. Predictor test: 1,000,000 rounds with 3 mines, 5 picks per round, played
   by five tile-picking strategies, including the "patterns" predictor apps sell.
3. Sessions: 100,000 sessions of 100 rounds at GBP 1 for four game settings.

Model assumptions
- Mines are placed uniformly at random before the first pick (as provably fair
  Mines games describe).
- Multiplier for k safe picks = RTP / P(clear k). Real games round multipliers
  down, so their true return can be fractionally below the nominal RTP.
- RTP = 97% (the figure listed for Spribe's Mines). Change RTP below to rerun.

Python 3, NumPy. Seed 20261025.
"""
import json
import csv
from math import comb

import numpy as np

SEED = 20261025
RTP = 0.97
TILES = 25
rng = np.random.default_rng(SEED)


def p_clear(mines: int, picks: int) -> float:
    """Probability that `picks` revealed tiles are all safe."""
    return comb(TILES - mines, picks) / comb(TILES, picks)


# ---------------------------------------------------------------- 1. exact odds
exact_rows = []
for m in range(1, 25):
    for k in range(1, TILES - m + 1):
        p = p_clear(m, k)
        exact_rows.append({
            "mines": m,
            "safe_picks": k,
            "p_clear": p,
            "fair_multiplier": 1 / p,
            "multiplier_at_rtp": RTP / p,
        })

# ------------------------------------------------------------ 2. predictor test
N_ROUNDS = 1_000_000
MINES = 3
PICKS = 5
WINDOW = 50  # rounds of history used by the "cold tiles" strategy
CHUNK = 100_000


def place_mines(n):
    """Boolean matrix (n, 25): True where a mine sits."""
    order = np.argsort(rng.random((n, TILES)), axis=1)
    board = np.zeros((n, TILES), dtype=bool)
    np.put_along_axis(board, order[:, :MINES], True, axis=1)
    return board


boards = np.concatenate([place_mines(CHUNK) for _ in range(N_ROUNDS // CHUNK)])


def pick_lowest(score):
    """Pick the PICKS tiles with the lowest score per row (ties broken at random)."""
    noisy = score + rng.random(score.shape) * 1e-3
    return np.argsort(noisy, axis=1)[:, :PICKS]


def cleared(picks):
    hit = np.take_along_axis(boards, picks, axis=1)
    return ~hit.any(axis=1)


results = {}

# a) fresh random tiles every round
results["random_tiles"] = cleared(pick_lowest(np.zeros((N_ROUNDS, TILES))))

# b) same five tiles every round: four corners and the centre
fixed = np.tile(np.array([0, 4, 12, 20, 24]), (N_ROUNDS, 1))
results["corners_and_centre"] = cleared(fixed)

# c) avoid the tiles where mines sat last round
prev = np.vstack([np.zeros((1, TILES), dtype=bool), boards[:-1]])
results["avoid_last_round_mines"] = cleared(pick_lowest(prev.astype(float)))[1:]

# d) "cold" tiles: the five with the fewest mines over the previous 50 rounds
cum = np.vstack([np.zeros((1, TILES), dtype=np.int32),
                 np.cumsum(boards, axis=0, dtype=np.int32)])
recent = cum[:-1] - cum[np.maximum(np.arange(N_ROUNDS) - WINDOW, 0)]
results["coldest_tiles_last_50"] = cleared(pick_lowest(recent.astype(float)))[WINDOW:]

# e) "mines never land twice": pick last round's three mine tiles plus two random
score = np.where(prev, -1.0, 0.0)
results["last_round_mine_tiles"] = cleared(pick_lowest(score))[1:]

expected = p_clear(MINES, PICKS)
predictor = {}
for name, arr in results.items():
    n = arr.size
    rate = arr.mean()
    se = np.sqrt(expected * (1 - expected) / n)
    predictor[name] = {
        "rounds": int(n),
        "cleared": int(arr.sum()),
        "clear_rate": float(rate),
        "z_vs_expected": float((rate - expected) / se),
    }

# per-tile mine frequency across all rounds (should be 3/25 = 12% each)
tile_freq = boards.mean(axis=0)

# ------------------------------------------------------------------ 3. sessions
N_SESSIONS = 100_000
ROUNDS = 100
STAKE = 1.0
settings = [(1, 3), (3, 5), (5, 10), (24, 1)]
sessions = {}
session_rows = []
for m, k in settings:
    p = p_clear(m, k)
    payout = RTP / p
    wins = rng.binomial(ROUNDS, p, N_SESSIONS)
    net = wins * payout * STAKE - ROUNDS * STAKE
    key = f"{m}_mines_{k}_picks"
    sessions[key] = {
        "mines": m,
        "safe_picks": k,
        "p_clear": p,
        "multiplier": payout,
        "mean_net": float(net.mean()),
        "median_net": float(np.median(net)),
        "share_ahead": float((net > 0).mean()),
        "share_lost_over_half": float((net <= -50).mean()),
        "best_session": float(net.max()),
        "worst_session": float(net.min()),
    }
    session_rows.append(sessions[key])

summary = {
    "seed": SEED,
    "rtp": RTP,
    "predictor_test": {
        "mines": MINES, "picks": PICKS, "expected_clear_rate": expected,
        "strategies": predictor,
        "tile_mine_frequency_min": float(tile_freq.min()),
        "tile_mine_frequency_max": float(tile_freq.max()),
    },
    "sessions": {"sessions": N_SESSIONS, "rounds": ROUNDS, "stake_gbp": STAKE,
                 "results": sessions},
}

with open("mines-summary.json", "w") as f:
    json.dump(summary, f, indent=2)

with open("mines-exact-odds.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=list(exact_rows[0]))
    w.writeheader()
    w.writerows(exact_rows)

with open("mines-predictor-test.csv", "w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["strategy", "rounds", "cleared", "clear_rate", "z_vs_expected"])
    for name, r in predictor.items():
        w.writerow([name, r["rounds"], r["cleared"], f"{r['clear_rate']:.6f}",
                    f"{r['z_vs_expected']:.3f}"])

print(json.dumps(summary, indent=2))
