5 Commits

Author SHA1 Message Date
thejayman77 369b1f59d4 Exercise short-stack calibration paths 2026-07-26 16:40:06 -04:00
thejayman77 0ac69b101a Replace bot tuning guesses with enforced calibration 2026-07-26 05:13:54 -04:00
thejayman77 fffd60ad9d Tune the skill axis: profiles now play like their labels
The reported symptom was a "Rock" at 41% VPIP against a 12% setting. Chasing it
uncovered four separate places where a *skill* parameter was smuggling in a
*style* change — the two axes were supposed to be independent.

1. Error direction. blunder() pushed every mistake the same way, so a 30% error
   rate put any beginner near a 30% VPIP floor regardless of style. Making it
   style-directed fixed the Rock but collapsed the gradient, because a tight
   player's errors then became folds, which cost almost nothing. Errors are now
   split: pre-flop follows the player's character (a nit's mistake is folding a
   hand they should have played), while post-flop stays costly for everyone —
   paying off when beaten and checking back hands worth betting. That is also
   where weak players genuinely lose money.

2. potOddsRespect shifted weak players systematically toward calling. That is
   not a weakness — calling wider than break-even against bad opponents is a
   winning adjustment, so it handed low-skill bots a real edge. Discipline now
   means ACCURACY: a weak player misjudges the threshold in either direction.

3. OpponentModel treated 0.5 as a neutral bet/raise share. Folds, checks and
   calls are counted too, so a normal player sits near 0.32 — every opponent
   read as passive, and the only two levels that consult the model tightened
   against the whole table and lost money for it. The exploitation feature was
   a handicap. Baseline calibrated and named.

4. positionAwareness widened 45% in position but narrowed 25% out of it. A seat
   is last to act about a quarter of the time, so the tighter branch dominated
   and higher awareness silently meant fewer hands. Position now shifts WHERE
   hands are played, not how many.

Also: the pre-flop slop multiplier now saturates (multiplying pushed a 0.75
maniac to 0.93 while still drowning out the tight end), raw pot odds carry an
implied-odds discount, and skill levels are re-spaced.

Results: Rock 41.3% -> 14.5% VPIP, every style ordered correctly by looseness,
and win rates down from ~113 to ~20 bb/100 for a strong seat.

HONEST LIMITATION: Advanced and Expert are not separable. Over 100k hands their
order flips with the seed. The simulator now asserts each level beats the one
two tiers below it — true on every seed tried — rather than strict adjacent
ordering, which would be reading noise as signal. Separating the top two needs
either a wider parameter gap or a different distinguishing mechanism.

New ProfileBehaviourTest is the regression that was missing: it asserts styles
actually produce their own behaviour. A gradient can look healthy while every
profile is misnamed, which is exactly what happened.

Tests: 154 -> 166 (75 engine JVM, 75 Android host, 16 app).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:52:13 -04:00
thejayman77 7f82251d86 Android target, suspending agents, and observable table snapshots
Build:
- :engine now uses com.android.kotlin.multiplatform.library (AGP 9.2.1), the
  modern KMP Android integration, rather than plain androidTarget(). Produces
  engine.aar alongside the JVM target; compileAndroidMain verified.
- Version catalog added; SDK levels match the other JSJ apps (compileSdk 37,
  minSdk 26).

Engine:
- PlayerAgent.act() and Table.playHand() are now suspend, so a human player can
  wait for input without blocking a thread. Bots are unaffected; the simulator
  wraps in runBlocking.
- TableSnapshot/SeatSnapshot published after the deal, before and after every
  action, and at the finish. Immutable, aliasing no live Seat state, giving
  animation, hand history, saving, and replay one boundary to work against.
- Snapshots carry the whole truth; maskedFor(viewer) is an explicit step that
  hides hole cards the viewer is not entitled to. Showdown reveals contenders;
  folded hands never are.
- Terminal snapshots report the contested pot rather than 0. settle() zeroes
  contributions when awarding, so the naive value was empty at exactly the
  moment the UI needs to show what was won. Caught by a new test.
- HumanAgent suspends on a CompletableDeferred and clears its pending state in a
  finally block, so cancelling an abandoned hand releases the wait instead of
  stranding it. Re-usable afterwards; a stale submit returns false.

Tests: 37 -> 45. Chips still conserved, skill gradient still monotonic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:55:28 -04:00
thejayman77 479be1f6b9 Initial commit: Hold'em engine, bots, and simulation harness
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>
2026-07-25 04:36:03 -04:00