도박사의 오류 ↔ 독립 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""도박사의 오류 ↔ 독립 브리지 문서의 수치를 검증한다.

1. 공정한 동전에서 뒷면 5번 연속 뒤 앞면의 확률 (모의실험)
2. 유럽식 룰렛(0 하나, 37칸) 빨강 베팅의 기댓값 -1/37 (정확값과 모의실험)
3. 마틴게일(잃으면 두 배로 걸기) 전략의 평균 손익과 파산 확률 (모의실험)
4. 로또 6/45 한 조합의 확률 1/C(45,6)
5. 큰 수의 법칙: 비율은 1/2로 가지만 개수 차이의 기댓값은 그대로다
"""
import random
from fractions import Fraction
from math import comb

random.seed(20260927)


def after_streak(n_flips: int, streak: int) -> float:
    """뒷면이 streak번 연속된 직후 던지기에서 앞면이 나온 비율."""
    run, hits, total = 0, 0, 0
    for _ in range(n_flips):
        heads = random.random() < 0.5
        if run >= streak:
            total += 1
            hits += heads
        run = 0 if heads else run + 1
    return hits / total


def roulette_red(n: int) -> float:
    """1단위씩 빨강에 n번 걸었을 때 한 판당 평균 손익."""
    net = 0
    for _ in range(n):
        slot = random.randrange(37)  # 0은 초록, 1~18 빨강, 19~36 검정으로 둔다
        net += 1 if 1 <= slot <= 18 else -1
    return net / n


def martingale(bankroll: int, spins: int) -> tuple[int, bool]:
    """빨강에 1부터 걸고 잃으면 두 배로 건다. 걸 돈이 모자라면 파산으로 멈춘다."""
    money, bet = bankroll, 1
    for _ in range(spins):
        if bet > money:
            return money - bankroll, True
        if 1 <= random.randrange(37) <= 18:
            money += bet
            bet = 1
        else:
            money -= bet
            bet *= 2
    return money - bankroll, False


if __name__ == "__main__":
    p = after_streak(2_000_000, 5)
    assert abs(p - 0.5) < 0.01, p
    print(f"뒷면 5연속 뒤 앞면 비율: {p:.4f}")

    ev = Fraction(18, 37) * 1 + Fraction(19, 37) * (-1)
    assert ev == Fraction(-1, 37)
    sim = roulette_red(2_000_000)
    assert abs(sim - float(ev)) < 0.003, sim
    print(f"빨강 베팅 기댓값: {ev} = {float(ev):.4f}, 모의실험 {sim:.4f}")
    # 1만 원씩 100판 걸면 기대 손실은 약 2만 7천 원
    loss_100 = 100 * 10_000 * float(ev)
    assert round(loss_100) == -27027, loss_100
    print(f"1만 원씩 100판의 기대 손익: {loss_100:,.0f}원")
    loss_c1 = 40 * 50_000 * float(ev)  # 확인 문제 C1
    assert round(loss_c1) == -54054, loss_c1
    print(f"5만 원씩 40판의 기대 손익: {loss_c1:,.0f}원")

    results = [martingale(100, 200) for _ in range(20_000)]
    mean_net = sum(r for r, _ in results) / len(results)
    ruin = sum(b for _, b in results) / len(results)
    assert mean_net < 0 and ruin > 0.5, (mean_net, ruin)
    print(f"마틴게일(자금 100, 최대 200판): 평균 손익 {mean_net:.1f}, 파산 비율 {ruin:.3f}")

    assert comb(45, 6) == 8_145_060
    print(f"로또 6/45 한 조합의 확률: 1/{comb(45, 6):,}")

    # 뒷면 5개가 앞선 상태에서 n번 더 던질 때: 앞면-뒷면 차이의 기댓값은 -5 그대로,
    # 앞면 비율의 기댓값은 n/2 / (n+5)로 1/2에 다가간다.
    for n in (10, 100, 10_000):
        expected_diff = -5 + 0  # 새 던지기의 앞면-뒷면 기댓값은 0
        expected_ratio = (n / 2) / (n + 5)
        assert expected_diff == -5
        print(f"n={n}: 차이 기댓값 {expected_diff}, 앞면 비율 기댓값 {expected_ratio:.4f}")
    assert abs((10_000 / 2) / 10_005 - 0.5) < 0.001
    print("ALL CHECKS PASSED")