← poker-tutor
import PokerCore
import TestKit
private func heroEquity(_ hero: String, vs villain: String, board: String = "", trials: Int = 40_000, seed: UInt64 = 7) -> Double {
var rng = SeededRandom(seed: seed)
return EquityCalculator.equity(
hero: [Card].parse(hero),
opponents: [[Card].parse(villain)],
board: [Card].parse(board),
trials: trials,
rng: &rng
).equity
}
func equityTests(_ t: TestRun) {
t.suite("equity") { t in
// Published all-in preflop equities. Tolerance is wide enough that sampling noise
// can't fail the suite, tight enough to catch a real error.
t.test("known preflop matchups") {
t.close(heroEquity("As Ah", vs: "Kc Kd"), 0.812, 0.015, "pair over pair")
t.close(heroEquity("As Ks", vs: "Qc Qd"), 0.460, 0.015, "suited overcards vs a pair")
t.close(heroEquity("As Ah", vs: "Kc Qd"), 0.870, 0.015, "pair vs two undercards")
t.close(heroEquity("Ac Ah", vs: "7c 2d"), 0.877, 0.015, "aces vs the worst hand")
t.close(heroEquity("Js Ts", vs: "Ac Kd"), 0.415, 0.015, "the flip that isn't a flip")
t.close(heroEquity("8c 8d", vs: "As Kh"), 0.545, 0.015, "small pair vs two overs")
}
t.test("identical hands split") {
t.close(heroEquity("As Ks", vs: "Ah Kh"), 0.50, 0.02)
}
t.test("equity falls as opponents are added") {
var rng = SeededRandom(seed: 11)
let hero = [Card].parse("Ac Ad")
var previous = 1.0
for count in 1...5 {
let e = EquityCalculator.equity(hero: hero, opponentCount: count, trials: 20_000, rng: &rng).equity
t.lessThan(e, previous, "aces should win less often against \(count) opponents")
previous = e
}
t.greaterThan(previous, 0.35, "aces should still be ahead five-handed")
}
t.test("drawing dead is zero") {
t.close(heroEquity("7c 2d", vs: "Ks Kd", board: "Kc Kh 9s 9d 4c", trials: 500), 0.0, 0.0001)
}
t.test("the nuts is one") {
t.close(heroEquity("As Ks", vs: "2c 2d", board: "Qs Js Ts 4h 3d", trials: 500), 1.0, 0.0001)
}
t.test("a chopped board is a tie") {
// Both play the board: broadway with no flush possible.
t.close(heroEquity("2c 3d", vs: "4c 5d", board: "As Kh Qd Jc Ts", trials: 500), 0.5, 0.0001)
}
t.test("same seed gives the same answer") {
t.equal(heroEquity("As Ah", vs: "Kc Kd", trials: 5_000, seed: 99),
heroEquity("As Ah", vs: "Kc Kd", trials: 5_000, seed: 99))
}
t.test("win, tie and lose sum to one") {
var rng = SeededRandom(seed: 3)
let r = EquityCalculator.equity(hero: [Card].parse("Ah Kd"), opponentCount: 3, trials: 5_000, rng: &rng)
t.close(r.win + r.tie + r.lose, 1.0, 0.0001)
}
}
}
func deckTests(_ t: TestRun) {
t.suite("deck and shuffle") { t in
t.test("shuffle is deterministic per seed") {
var a = SeededRandom(seed: 12345)
var b = SeededRandom(seed: 12345)
var c = SeededRandom(seed: 54321)
var deckA = Deck(rng: &a), deckB = Deck(rng: &b), deckC = Deck(rng: &c)
let orderA = deckA.deal(52)
t.equal(orderA, deckB.deal(52), "same seed, same deck")
t.notEqual(deckC.deal(52), orderA, "different seed, different deck")
}
t.test("the deck holds every card exactly once") {
var rng = SeededRandom(seed: 8)
var deck = Deck(rng: &rng)
t.equal(deck.remaining, 52)
let dealt = deck.deal(52)
t.equal(Set(dealt).count, 52)
t.equal(deck.remaining, 0)
}
t.test("a stacked deck deals in order, then the rest") {
var deck = Deck(stacked: [Card].parse("As Ks Qs"))
t.equal(deck.deal(3).shorthand, "As Ks Qs")
t.equal(deck.remaining, 49)
}
t.test("the shuffle is not biased toward the top") {
var counts = [Int](repeating: 0, count: 52)
for seed in 0..<5_200 {
var rng = SeededRandom(seed: UInt64(seed))
var deck = Deck(rng: &rng)
counts[Int(deck.deal().index)] += 1
}
let low = counts.min() ?? 0, high = counts.max() ?? 0
t.greaterThan(low, 55, "some card almost never lands on top")
t.lessThan(high, 155, "some card lands on top too often")
}
}
}