Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions PokerKit/Tests/PokerKitTests/EquityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<remaining.count {
for j in (i + 1)..<remaining.count {
let finalHand = HandEvaluator.bestHand(from: [hero.first, hero.second] + flop + [remaining[i], remaining[j]])
if finalHand.category >= .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)
}
28 changes: 28 additions & 0 deletions ai-docs/EQUITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading