"""Plinko: exact binomial landing distribution per row count, checked against 1M simulated balls.""" import numpy as np from math import comb from common import save SEED = 20261004 rng = np.random.default_rng(SEED) N = 1_000_000 out = [] rows_csv = [] for rows in range(8, 17): exact = [comb(rows, k) / 2 ** rows for k in range(rows + 1)] sim = np.bincount(rng.binomial(rows, 0.5, N), minlength=rows + 1) / N edge_pct = exact[0] * 2 * 100 mid = [rows // 2 - 1, rows // 2, rows // 2 + 1] if rows % 2 == 0 else [rows // 2, rows // 2 + 1] centre3 = sum(exact[i] for i in mid) * 100 out.append({"rows": rows, "exact_pct": [round(x * 100, 5) for x in exact], "sim_pct": [round(x * 100, 5) for x in sim], "either_edge_slot_pct": round(edge_pct, 5), "one_in_edge": round(1 / (exact[0] * 2)), "centre_slots": len(mid), "centre_pct": round(centre3, 2)}) for k in range(rows + 1): rows_csv.append([rows, k, round(exact[k] * 100, 6), round(sim[k] * 100, 6)]) # streaks: how often 100 balls in a row (16 rows) miss the outer 4 slots on each side p_outer = sum(comb(16, k) for k in (0, 1, 2, 3)) * 2 / 2 ** 16 streak = {"rows": 16, "p_outer_4_each_side_pct": round(p_outer * 100, 4), "chance_none_in_100_balls_pct": round((1 - p_outer) ** 100 * 100, 2), "expected_balls_between_outer_hits": round(1 / p_outer, 1)} save("plinko", {"game": "Plinko", "balls_per_row_setting": N, "seed": SEED, "note": "Landing odds only. Payout tables differ by operator, so RTP must be computed from each game's published table.", "script": "plinko_sim.py"}, {"distribution": out, "streak": streak}, rows_csv, ["rows", "slot", "exact_pct", "simulated_pct"])