The KDoc claimed the worst hand is percentile 1.0, but percentiles mark the
START of each class's band, so the last class begins at (1326-12)/1326 ~ 0.991
and no real hand reaches 1.0. Documents that this is correct for gating, since a
class is admitted when its band opens inside the range. Also notes that the
unused key slots keep 1.0 as an unreachable sentinel.
Docs only; no behaviour change. 37 tests still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TDA Rule 47 — cumulative incomplete raises:
A boolean could not express "facing at least a full raise since acting", so
several short all-ins that together reached a full raise failed to reopen
betting. Seat now records lastActedAtBet (the currentBet when it last acted);
mayRaise() reopens when currentBet - lastActedAtBet >= minRaiseSize. This
subsumes the single-incomplete-raise case, so the hasActed reset in apply() is
gone.
TDA Rule 20 — odd chips:
Split-pot remainders were awarded in seat-list order. They now go to the first
winning seat clockwise from the button. The old test also never produced an odd
pot (20 chips heads-up), so it only ever proved an even split; it now builds a
genuinely odd 25-chip pot via a folded small blind and asserts which seat takes
the extra chip.
OpponentModel skipped events across hands:
It inferred a new hand from a shrinking history, but history is cleared each
hand: having consumed 3 events, first observing the next hand at 4 events left
4 < 3 false and silently dropped the first three. observe() now takes an
explicit handNumber, exposed via DecisionContext and Table.handNumber.
PreflopChart percentile semantics:
The 169 classes were ranked equally, but they are not equally likely — a pair is
6 of 1326 combinations, suited 4, offsuit 12. "Top 12%" therefore meant 12% of
classes, not of dealt hands, so looseness did not mean what it claimed.
Percentiles are now weighted by combination count.
Tests: 30 -> 37. Each new test was verified to fail with its fix reverted.
Skill gradient still monotonic: 85.9 / 79.8 / 17.4 / -183.1 bb/100 over 50k
hands, chips conserved on both tables.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kotlin Multiplatform engine (JVM target only for now; androidTarget and
iosArm64 slot in without touching commonMain).
Core:
- HandEvaluator: single-pass 5-7 card evaluation, ~24M evals/sec. Verified
exhaustively against published frequencies for all 2,598,960 five-card hands.
- Equity: Monte Carlo with ties split. PreflopChart ranks the 169 starting
hands using all-in equity plus an explicit playability adjustment, so
looseness means "plays the top N%".
- Table: no-limit betting rounds, side pots, odd-chip splits, uncalled-bet
refunds, and incomplete (short all-in) raises that correctly do not reopen
betting.
Bots:
- SkillLevel and PlayStyle are orthogonal axes. Skill drives decision quality
(rollout accuracy, pot-odds discipline, position awareness, error rate);
style drives bluffing, sandbagging, aggression, tightness.
- BotMood gives tilt that persists between hands and decays.
- OpponentModel lets Advanced/Expert exploit habitual bettors.
- MathBot emits a DecisionTrace of the numbers behind each decision, which the
coach will later hand to an LLM to narrate. The LLM never does poker maths.
Simulator:
- 2,200-3,400 hands/sec. Deck RNG is separate from bot RNGs so rollout counts
cannot shift the deal.
- Controlled skill-ladder test asserts the difficulty gradient is monotonic:
73.9 / 53.9 / 27.6 / -155.4 bb/100 over 50k hands.
Assets: 52 CC0 English-pattern card faces plus generated backs.
Tests: 30 passing (evaluator, table rules, pre-flop chart).
Known open: win-rate magnitudes ~10x realistic and several profiles looser
than their labels. Tuning, not correctness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>