Skip to content

Add equity calculator (hand evaluator + exact/Monte Carlo engine) - #6

Merged
testtest126 merged 2 commits into
mainfrom
feature/equity-calculator
Jul 21, 2026
Merged

Add equity calculator (hand evaluator + exact/Monte Carlo engine)#6
testtest126 merged 2 commits into
mainfrom
feature/equity-calculator

Conversation

@testtest126

Copy link
Copy Markdown
Owner

Summary

Pure-math feature: a real poker hand evaluator + equity calculator, validated against published ground-truth numbers (not hand-waved).

  • HandEvaluator — best 5-card poker hand from 5-7 cards, derived from actual rank/suit counts (no lookup table). 24 tests: full category ladder, the wheel (5-high, not ace-high), kicker tie-breaks, 6/7-card best-of selection.
  • EquityheadsUp (exact, full board enumeration — zero sampling error, tractable up to preflop's 1,712,304 boards) for hand vs. hand; monteCarlo/handVsRange/rangeVsRange (fixed-seed sampling) for anything involving a range; canonicalVsCanonical/expandCanonical for combo-weighted canonical-hand equity (see below — this turned out to matter).
  • EquityCalculatorView — hero/villain canonical-notation entry, optional board, win/tie/lose %. Wired into the home screen.

Ground-truth validation (the actual point)

All 5 required matchups, cross-checked against cardfight.com (fetched live while building this, not recalled from memory):

Matchup Cited Measured Diff
AA vs KK 81.71% / 17.82% / 0.46% 81.92% / 17.64% / 0.44% 0.21pt
AKs vs QQ 45.83% / 53.73% / 0.43% 45.78% / 53.81% / 0.41% 0.05pt
AKo vs 22 47.04% / 52.34% / 0.62% 47.04% / 52.39% / 0.58% 0.00pt
AA vs 72o 87.99% / 11.59% / 0.42% 87.78% / 11.79% / 0.43% 0.21pt
QQ vs JTs (overpair vs. suited connector) 81.47% / 18.13% / 0.40% 81.32% / 18.28% / 0.40% 0.15pt

All within 0.25pt — well inside the asserted ±1.0% tolerance. Full citations and methodology in ai-docs/EQUITY.md.

A genuine finding, not just confirmation: the popular "AA vs KK ≈ 82.4%" figure is a combo-weighted average across all ways the two hands' suits can relate (0/1/2 shared), not the equity of one arbitrary suit assignment. A single specific-suit exact computation (HoleCards(canonical:)'s default clubs+diamonds pair) measured 82.36%, while a non-overlapping-suit assignment measured 81.06% — a real, reproducible ~1.3pt swing from suits alone, both via exact zero-error enumeration. canonicalVsCanonical/expandCanonical exist specifically to compute the number published sources actually mean. Documented in EQUITY.md's "A subtlety: which suits?" section — this is the difference between confirming a number looks right and actually checking it's answering the same question.

Also caught and fixed: two of the task's own reference numbers ("AKo vs 22 ≈ 50%", "AA vs 72o ≈ 88%+") are loose colloquial roundings — the precise figures (47.04%/52.34% and 87.99%/11.59%) don't literally satisfy those descriptions read strictly. Tests assert against the precise citations, not the rounded framing; noted honestly in EQUITY.md rather than silently loosening the test to match.

Bugs found and fixed during UI verification

Three real issues surfaced by driving the UI tests to green (not just "code compiles"):

  1. Notation-case bug: heroNotation.uppercased() corrupted "AKs" → "AKS", which expandCanonical can't parse (needs lowercase s/o) — silently invalidated the default hand. Fixed with a normalizer that uppercases ranks but not the suited/offsuit flag.
  2. View-struct async footgun: originally mutated @State from a Task.detached/GCD closure capturing self on the View struct — refactored to a small @Observable reference-type model, the textbook-correct pattern for background work reporting back to a SwiftUI value-type view.
  3. SwiftUI Form/List lazy rendering: rows below the fold aren't in the accessibility tree until scrolled into view — affected the new Result section AND, incidentally, the pre-existing BankrollTrackerUITests (inserting a new home-screen row pushed "Bankroll Tracker" past the same boundary). Both fixed with a scroll-until-visible helper.

Testing

  • swift test: 177/177 passing (135 baseline + 42 new: 24 HandEvaluator + 18 Equity). Zero lost.
  • Performance note: one test (exactPreflopEnumerationAAvsKK) deliberately pays the full ~5-minute exact-preflop-enumeration cost to prove that path works end-to-end; every ground-truth check uses fast Monte Carlo instead. Documented in EQUITY.md.
  • App: built clean on iOS Simulator (Xcode 27 beta); all 7 UI test suites pass, including 3 new EquityCalculatorUITests and the fixed BankrollTrackerUITests.

What this doesn't do

No range-parsing beyond single canonical hand strings, no postflop simulation (pure probability, given a board), no ICM — all noted in EQUITY.md.

HandEvaluator ranks the best 5-card poker hand from 5-7 cards (real
category+kicker derivation from rank/suit counts, no lookup table) — the
foundation everything else builds on. 24 tests cover the full category
ladder, the wheel (5-high, not ace-high), kicker tie-breaks, and 6/7-card
best-of selection.

Equity computes win/tie/lose probability two ways: headsUp is exact (full
board enumeration, zero sampling error, tractable up to preflop's
1,712,304 boards) for one hand vs one hand; monteCarlo (plus
handVsRange/rangeVsRange convenience wrappers) is fixed-seed sampling for
anything involving a range. canonicalVsCanonical/expandCanonical compute
the combo-weighted equity published references like "AA vs KK" actually
mean, not one arbitrary suit assignment's equity — see EQUITY.md's "A
subtlety: which suits?" for a real ~1pt discrepancy this surfaced and how
it's handled.

Validated against 5 independently-fetched cardfight.com ground-truth
figures (AA vs KK, AKs vs QQ, AKo vs 22, AA vs 72o, QQ vs JTs), all within
0.25pt — see EQUITY.md for the full comparison table, sources, and a
performance note (one deliberately-isolated exact-preflop-enumeration test
takes ~5 minutes in a debug build; documented, not hidden).
New StudyTool.equityCalculator entry (between Push/Fold and Bankroll) and
EquityCalculatorView: canonical-notation hero/villain entry, an optional
board (street picker + rank/suit pickers per card), and a Calculate button
showing win/tie/lose %.

Always uses Monte Carlo (10k iterations), never headsUp's exact
enumeration — the exact preflop path takes minutes even in release, which
would hang the screen. Computation runs off the main thread via a small
@observable model class rather than mutating View-struct @State directly
from an escaping closure — capturing `self` for a value-type View across
an async boundary is a documented footgun (a completion handler can write
into a stale struct copy that never reaches the screen), and this sidesteps
it by giving the async work a single stable reference-type identity to
report back to instead.

Also fixes two SwiftUI Form/List lazy-rendering gotchas surfaced by the new
UI tests: rows below the fold don't exist in the accessibility tree until
scrolled into view, which affected both EquityCalculatorUITests (the
Result section) and, incidentally, the pre-existing BankrollTrackerUITests
(inserting Equity Calculator into the home list pushed Bankroll Tracker
past the same boundary on this screen size).
@testtest126
testtest126 merged commit 7b03bd8 into main Jul 21, 2026
2 checks passed
@testtest126
testtest126 deleted the feature/equity-calculator branch July 21, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant