"""Finite-horizon inability to fund another fixed one-unit wager.

Educational independent-round model only. Targets1.50/2.00 use p=.97/target,
equality wins, no fees, no deposits, no stop target, no stake changes. A balance
below one unit is absorbing, including after the last allowed round. The1.50
model may stop with half a unit left; this is not necessarily a zero balance.
Usage: python finite_bankroll.py
"""
from fractions import Fraction
import json
import logging


def fixed_stake_failure(bankroll_units, target_cents, rounds, exact=False):
    if not isinstance(bankroll_units, int) or bankroll_units < 1:
        raise ValueError('Positive integer initial betting units required')
    if target_cents not in (150, 200):
        raise ValueError('Only targets1.50 and2.00 are implemented')
    if not isinstance(rounds, int) or rounds < 0:
        raise ValueError('Nonnegative integer horizon required')
    p = Fraction(97, target_cents) if exact else 97 / target_cents
    q = 1 - p
    zero = Fraction(0) if exact else 0.0
    active = {bankroll_units * 2: 1 - zero}
    absorbed = zero
    # Balances use half-unit integers. One stake costs two half-units.
    win_step = 1 if target_cents == 150 else 2
    for _ in range(rounds):
        following = {}
        for balance, mass in active.items():
            for after, probability in ((balance + win_step, p), (balance - 2, q)):
                moved = mass * probability
                if after < 2:
                    absorbed += moved
                else:
                    following[after] = following.get(after, zero) + moved
        active = following
    total = absorbed + sum(active.values(), zero)
    if exact:
        assert total == 1
    else:
        assert abs(total - 1) < 1e-11
    return absorbed


def reference_table():
    return [dict(target=target / 100, initialUnits=units,
                 failureWithin50=float(fixed_stake_failure(units, target, 50)),
                 failureWithin100=float(fixed_stake_failure(units, target, 100)),
                 failureWithin200=float(fixed_stake_failure(units, target, 200)))
            for target in (150, 200) for units in (10, 20, 50, 100)]


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    logging.info(json.dumps(dict(scope='Independent educational model; not a casino study',
                                failure='balance below next one-unit stake',
                                results=reference_table()), indent=2))
