"""Reproduce the numbers and synthetic dot sample in the randomness infographic.

Python 3 standard library only. Run: python3 calculations.py
Use --verify to additionally enumerate all 1,048,576 twenty-flip sequences.
Outputs are written beside this script. No network calls or observed player data.
"""
from fractions import Fraction
from itertools import product
from pathlib import Path
import argparse
import csv
import json
import random


def heads_run_probability(n: int, k: int = 5) -> Fraction:
    """P(at least k consecutive heads in n independent fair coin flips).

    states[j] counts sequences with no qualifying run, ending in j heads.
    A tail resets the trailing run; a head extends it. Sequences reaching
    k heads leave the no-run states. Integer counts avoid rounding drift.
    """
    if n < 0 or k < 1:
        raise ValueError("Require n >= 0 and k >= 1")
    states = [1] + [0] * (k - 1)
    for _ in range(n):
        states = [sum(states)] + states[:-1]
    return Fraction(2**n - sum(states), 2**n)


def exact_dict(value: Fraction) -> dict:
    return {"numerator": value.numerator, "denominator": value.denominator,
            "probability": float(value), "percent": float(value * 100)}


def make_data() -> dict:
    rng = random.Random(20260927)
    samples = []
    for seed in range(20260927, 20260939):
        sample_rng = random.Random(seed)
        samples.append({"seed": seed, "points": [[sample_rng.random(), sample_rng.random()] for _ in range(64)]})
    return {
        "title": "What randomness actually looks like",
        "version": "2.0", "date": "2026-09-27",
        "assumptions": {
            "coins": "Independent fair coin flips; a run means heads, not either side.",
            "cards": "Uniform random order of 52 distinct cards; 4 Aces; no replacement.",
            "dots": "64 synthetic points per sample with independent uniform x and y coordinates. Python random.Random seeds 20260927 through 20260938 in order, without seed selection. Poster uses the first sample. Not observed data or a generator fairness test."
        },
        "exact_six_flip_sequence": exact_dict(Fraction(1, 64)),
        "next_five_heads": exact_dict(Fraction(1, 32)),
        "five_heads_anywhere_in_twenty": exact_dict(heads_run_probability(20)),
        "ace_before_draw": exact_dict(Fraction(4, 52)),
        "ace_after_ace_removed": exact_dict(Fraction(3, 51)),
        "ace_after_non_ace_removed": exact_dict(Fraction(4, 51)),
        "dots": [[rng.random(), rng.random()] for _ in range(64)],
        "dot_samples": samples,
        "run_curve": [{"flips": n, **exact_dict(heads_run_probability(n))}
                      for n in range(5, 101)],
    }


def verify() -> None:
    for n in range(13):
        for k in range(1, 7):
            count = sum("H" * k in "".join(seq)
                        for seq in product("HT", repeat=n))
            assert heads_run_probability(n, k) == Fraction(count, 2**n)
    # Separate exhaustive check, using a bitwise run detector rather than recurrence.
    count20 = sum(bool(x & (x >> 1) & (x >> 2) & (x >> 3) & (x >> 4))
                  for x in range(2**20))
    assert count20 == 262008
    assert heads_run_probability(20) == Fraction(count20, 2**20)
    assert heads_run_probability(5) == Fraction(1, 32)
    assert heads_run_probability(20) == Fraction(32751, 131072)
    # Total probability over the first card recovers the initial Ace chance.
    assert Fraction(4, 52)*Fraction(3, 51) + Fraction(48, 52)*Fraction(4, 51) == Fraction(4, 52)
    data = make_data()
    assert len(data["dots"]) == 64
    assert all(0 <= c < 1 for point in data["dots"] for c in point)
    assert len(data['dot_samples']) == 12
    assert data['dot_samples'][0]['points'] == data['dots']
    assert all(len(s['points']) == 64 and all(0 <= c < 1 for p in s['points'] for c in p) for s in data['dot_samples'])
    assert all(a["probability"] <= b["probability"]
               for a, b in zip(data["run_curve"], data["run_curve"][1:]))
    print("PASS: exhaustive n=0..12, k=1..6; all 1,048,576 twenty-flip sequences; card identity; twelve 64-point samples; monotone curve.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--verify", action="store_true")
    args = parser.parse_args()
    if args.verify:
        verify()
    root = Path(__file__).resolve().parent
    data = make_data()
    (root / "data.json").write_text(json.dumps(data, indent=2) + "\n")
    with (root / "streak-probabilities.csv").open("w", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=["flips", "numerator", "denominator", "probability", "percent"], lineterminator="\n")
        writer.writeheader()
        writer.writerows(data["run_curve"])
    for key in ["exact_six_flip_sequence", "next_five_heads", "five_heads_anywhere_in_twenty", "ace_before_draw", "ace_after_ace_removed", "ace_after_non_ace_removed"]:
        print(f"{key}: {data[key]['percent']:.9f}%")
