#!/usr/bin/env python3
"""Follow-up to betting-systems-sim.py, prompted by a r/probabilitytheory reply
(u/Haruspex12, 2026-08-29):

  1) "Plot the kernel density of the outcomes. There are two types of wins,
     small ones and near total losses."  -> check bimodality of the Fibonacci
     final-balance distribution directly.
  2) "Sometimes, if I am playing red and you black ... if we both lose from the
     house edge, the profit to the house doubles."  -> simulate two Fibonacci
     players on the SAME wheel, one on red, one on black, and compare the
     house's take and its variance against two independent players.

Game and session rules identical to betting-systems-sim.py:
  European roulette even-money (p = 18/37), bankroll $200, base unit $1,
  table max $500, 300 rounds per session, bust when the required bet cannot
  be covered. Seed 20260830.

Key structural facts the simulation should (and does) confirm:
  - EV: the house's expected take is edge x total turnover in BOTH setups;
    pairing changes correlation/variance, never the mean rate.
  - Zeros: on a zero (prob 1/37) both players lose simultaneously - the only
    spin outcome where the house books both bets at once.
  - Anticorrelation: on red/black spins one player's win is the other's loss,
    so the house's per-spin take is the DIFFERENCE of their (diverging) ladder
    bets, plus the occasional double hit on zero.
"""
import random
from statistics import mean, median

SEED = 20260830
P_WIN = 18 / 37
P_ZERO = 1 / 37
EDGE = 1 / 37

BANKROLL = 200
UNIT = 1
TABLE_MAX = 500
ROUNDS = 300
N_SESS = 200_000


class Fib:
    __slots__ = ('bal', 'wagered', 'ladder', 'i', 'bust')

    def __init__(self):
        self.bal = float(BANKROLL)
        self.wagered = 0.0
        self.ladder = [1, 1]
        self.i = 0
        self.bust = False

    def next_bet(self):
        bet = min(self.ladder[self.i] * UNIT, TABLE_MAX)
        if bet > self.bal:
            self.bust = True
            return None
        return bet

    def settle(self, bet, won):
        self.bal += bet if won else -bet
        self.wagered += bet
        if won:
            self.i = max(0, self.i - 2)
        else:
            self.i += 1
            while self.i >= len(self.ladder):
                self.ladder.append(self.ladder[-1] + self.ladder[-2])


def paired_session(rng):
    """Two Fibonacci players, same wheel: A on red, B on black."""
    a, b = Fib(), Fib()
    house = 0.0
    for _ in range(ROUNDS):
        if a.bust and b.bust:
            break
        bet_a = a.next_bet() if not a.bust else None
        bet_b = b.next_bet() if not b.bust else None
        if bet_a is None and bet_b is None:
            break
        r = rng.random()
        # one spin: red with p 18/37, black 18/37, zero 1/37
        red = r < P_WIN
        zero = r >= 2 * P_WIN
        if bet_a is not None:
            won_a = red and not zero
            a.settle(bet_a, won_a)
            house += -bet_a if won_a else bet_a
        if bet_b is not None:
            won_b = (not red) and not zero
            b.settle(bet_b, won_b)
            house += -bet_b if won_b else bet_b
    return a, b, house


def independent_session(rng):
    """Two Fibonacci players on independent wheels (baseline)."""
    a, b = Fib(), Fib()
    house = 0.0
    for p in (a, b):
        for _ in range(ROUNDS):
            bet = p.next_bet()
            if bet is None:
                break
            won = rng.random() < P_WIN
            p.settle(bet, won)
            house += -bet if won else bet
    return a, b, house


def summarize(name, results):
    houses = [h for _, _, h in results]
    finals = [p.bal for a, b, _ in results for p in (a, b)]
    busts = sum(1 for a, b, _ in results for p in (a, b) if p.bust)
    both_bust = sum(1 for a, b, _ in results if a.bust and b.bust)
    turnover = sum(a.wagered + b.wagered for a, b, _ in results)
    n = len(results)
    print(f"\n{name}  ({n:,} sessions, {2*n:,} player-sessions)")
    print(f"  house take: mean ${mean(houses):8.2f}/session   median ${median(houses):8.2f}")
    print(f"  house take as % of turnover: {100*sum(houses)/turnover:.3f}%  (edge = {100*EDGE:.3f}%)")
    var = mean([(h - mean(houses)) ** 2 for h in houses])
    print(f"  house per-session std dev: ${var ** 0.5:.2f}")
    print(f"  player bust rate: {100*busts/(2*n):.2f}%   both-bust sessions: {100*both_bust/n:.2f}%")
    print(f"  player final balance: mean ${mean(finals):.2f}  median ${median(finals):.2f}")
    return finals


def histogram(finals, label):
    print(f"\n  final-balance distribution ({label}):")
    edges = [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250, 300, 10_000]
    n = len(finals)
    for lo, hi in zip(edges, edges[1:]):
        c = sum(1 for f in finals if lo <= f < hi)
        bar = '#' * round(60 * c / n)
        tag = f"${lo}-{hi}" if hi < 10_000 else f"${lo}+"
        print(f"    {tag:>10}: {100*c/n:6.2f}%  {bar}")


def main():
    rng = random.Random(SEED)
    print(f"seed {SEED}; bankroll ${BANKROLL}, unit ${UNIT}, table max ${TABLE_MAX}, "
          f"{ROUNDS} rounds/session, Fibonacci both players")

    paired = [paired_session(rng) for _ in range(N_SESS)]
    finals_p = summarize("PAIRED (A on red, B on black, same wheel)", paired)

    indep = [independent_session(rng) for _ in range(N_SESS)]
    finals_i = summarize("INDEPENDENT (two separate wheels)", indep)

    histogram(finals_i, "independent Fibonacci players - bimodality check")

    zeros_hit_both = mean([1 if (a.bust and b.bust) else 0 for a, b, _ in paired])
    print(f"\nnote: in the paired setup the only single-spin event where the house "
          f"books both bets is zero (p = {100*P_ZERO:.2f}% per spin).")
    print(f"both-bust rate, paired: {100*zeros_hit_both:.2f}% of sessions")


if __name__ == '__main__':
    main()
