From 8c4de39c83e5717658feaf09f89f9fb6ca62322e Mon Sep 17 00:00:00 2001 From: Yakiv Kovalskyi Date: Tue, 21 Jul 2026 19:02:41 +0200 Subject: [PATCH] Validate Precise mode against a known postflop equity number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the postflop counterpart to the preflop ground-truth suite: a 9-out flush draw's exact completion rate, 1 - (38/47) x (37/46) ~= 34.97%, the single most rigorously-citable postflop statistic in poker math (exact combinatorics, not scraped from a site). Measured 378/1081 = 34.968% by exhaustively enumerating all C(47,2) turn/river combinations directly through HandEvaluator, with no Equity API involved — isolates the hand-ranking machinery Precise mode depends on from a 0.002-point margin. A second test cross-checks the same flush draw against a set on the same flop through the actual Equity.headsUp API, against a commonly-cited rough "75/25" figure for that matchup (75.35%/24.65% measured, zero ties) — wider tolerance since that citation is an approximation, not exact math, but confirms the Equity API surfaces the underlying verified math correctly. swift test: 183/183 (181 baseline + 2 new). No source changes — Precise mode's existing implementation is unchanged; this closes the "validated against a known postflop equity number" gap in its original validation. --- .../Tests/PokerKitTests/EquityTests.swift | 57 +++++++++++++++++++ ai-docs/EQUITY.md | 28 +++++++++ 2 files changed, 85 insertions(+) diff --git a/PokerKit/Tests/PokerKitTests/EquityTests.swift b/PokerKit/Tests/PokerKitTests/EquityTests.swift index 61d047f..2b3f2f3 100644 --- a/PokerKit/Tests/PokerKitTests/EquityTests.swift +++ b/PokerKit/Tests/PokerKitTests/EquityTests.swift @@ -276,3 +276,60 @@ private func assertApproximately(_ actual: Double, _ expectedPercent: Double, to #expect(direct.tieRate == viaRange.tieRate) #expect(direct.trials == viaRange.trials) } + +// MARK: - Postflop ground-truth validation +// +// Everything above validates preflop equity against published references. This section +// does the same job postflop, against the single most rigorously-citable postflop +// statistic in poker math: a 9-out flush draw's exact completion rate. + +@Test func postflopGroundTruthFlushDrawCompletionRate() { + // Widely cited across poker literature (the "Rule of 4-and-2"'s exact source figure, + // not an approximation of it): a 9-out draw with two cards to come completes + // `1 - (38/47) × (37/46) ≈ 34.97%` of the time — exact combinatorics, not an estimate, + // so this is checkable independently of any external site or Monte Carlo run. + // + // Hero holds a naked flush draw — no pair, no straight draw live on this specific flop + // (A♠K♠ against 2♠7♠9♣ doesn't connect to a gutshot or open-ender) — so "hero's final + // hand is a flush or better" measures exactly the 9-spades-remaining completion rate + // the citation describes, isolated from any other way to improve. + let hero = HoleCards(Card(rank: .ace, suit: .spades), Card(rank: .king, suit: .spades))! + let flop = [Card(rank: .two, suit: .spades), Card(rank: .seven, suit: .spades), Card(rank: .nine, suit: .clubs)] + + let used = Set([hero.first, hero.second] + flop) + let remaining = Equity.fullDeck.filter { !used.contains($0) } + #expect(remaining.count == 47) + + var completions = 0 + var total = 0 + for i in 0..= .flush { completions += 1 } + total += 1 + } + } + + #expect(total == 1081) // C(47, 2) + let completionRate = Double(completions) / Double(total) * 100 + #expect(abs(completionRate - 34.97) < 0.5, "expected ~34.97%, got \(completionRate)%") +} + +@Test func postflopGroundTruthFlushDrawVsSetViaEquityEngine() { + // A secondary, end-to-end cross-check through the actual Equity API (not just + // HandEvaluator directly): "a set vs. a flush draw is roughly a 75/25 spot" is a + // commonly-cited approximate figure (found via web search while building this) — looser + // than the exact 34.97% draw-math above, so checked to a wider tolerance. The set can + // also improve to a boat/quads and the flush isn't automatically the winner even when it + // completes, so this isn't expected to land on exactly 65.03% (100 - 34.97) for hero; + // it's a corroborating check that the Equity engine's output is in the right + // neighborhood as a published rough figure, not the primary precision claim. + let hero = HoleCards(Card(rank: .ace, suit: .spades), Card(rank: .king, suit: .spades))! + let villain = HoleCards(Card(rank: .nine, suit: .diamonds), Card(rank: .nine, suit: .hearts))! + let flop = [Card(rank: .two, suit: .spades), Card(rank: .seven, suit: .spades), Card(rank: .nine, suit: .clubs)] + + let result = Equity.headsUp(hero: hero, villain: villain, board: flop) + #expect(result.isExact) + assertApproximately(result.loseRate, 75, tolerance: 5) + assertApproximately(result.winRate, 25, tolerance: 5) +} diff --git a/ai-docs/EQUITY.md b/ai-docs/EQUITY.md index 2498281..40030b1 100644 --- a/ai-docs/EQUITY.md +++ b/ai-docs/EQUITY.md @@ -238,6 +238,34 @@ A note on the task's original framing of two of these: code. Fixed by testing against the precise citation instead of the rounded description, same as the other two notes here. +### Postflop + +Everything above is preflop. Postflop equity is validated against the single +most rigorously-citable statistic in poker math — not scraped from a +website, but exact combinatorics independently reproducible by anyone: + +**A 9-out draw with two cards to come completes `1 - (38/47) × (37/46) ≈ +34.97%` of the time.** `postflopGroundTruthFlushDrawCompletionRate` sets up a +naked flush draw (`A♠K♠` on a `2♠7♠9♣` flop — no pair, no straight draw live, +so "final hand is a flush or better" isolates exactly the 9-remaining-spades +statistic) and exhaustively enumerates all `C(47,2) = 1,081` turn/river +combinations directly through `HandEvaluator`, with no `Equity` API in the +loop at all. Measured: **378/1,081 = 34.968%** — a 0.002-point difference +from the cited figure, i.e. as close to an exact match as floating-point +rounding allows. + +A second test, `postflopGroundTruthFlushDrawVsSetViaEquityEngine`, closes the +loop through the actual `Equity.headsUp` API: the same flush draw against a +set on the same flop (`9♦9♥` — trips using the board's `9♣`) is commonly +cited (found via web search) as **"roughly a 75/25 spot"** favoring the set. +Measured via `headsUp`: **75.35% / 24.65%** (zero ties on this specific +board). This citation is a rounded approximation, not exact math like the +34.97% figure above — the set can also improve to a boat/quads, and a +completed flush isn't an automatic win — so it's checked to a wider `±5pt` +tolerance and treated as a corroborating check that the `Equity` API surfaces +the underlying (already independently-verified) `HandEvaluator` math +correctly, not as a second precision claim. + ## Performance Measured on this machine, Swift 6.3 (Xcode 27 beta toolchain), Apple