← poker-tutor
import Foundation
public struct EquityResult: Sendable {
public let win: Double
public let tie: Double
public let lose: Double
public let trials: Int
/// Share of the pot this hand expects to win, counting split pots proportionally.
public var equity: Double { win + tie }
}
/// Monte Carlo equity. Deterministic for a given seed so the coach's numbers are reproducible
/// when a hand is replayed.
public enum EquityCalculator {
/// Hero's equity against a set of opponents. An opponent given as `[]` holds unknown cards.
public static func equity(
hero: [Card],
opponents: [[Card]],
board: [Card] = [],
trials: Int = 10_000,
rng: inout SeededRandom
) -> EquityResult {
precondition(hero.count == 2, "hero needs two hole cards")
precondition(board.count <= 5, "board can hold at most five cards")
precondition(!opponents.isEmpty, "need at least one opponent")
var dead = Set(hero + board)
for o in opponents { dead.formUnion(o) }
let stub = Card.all.filter { !dead.contains($0) }
let boardNeeded = 5 - board.count
let unknownSeats = opponents.enumerated().filter { $0.element.isEmpty }.map(\.offset)
let drawCount = boardNeeded + unknownSeats.count * 2
var wins = 0.0, ties = 0.0, losses = 0.0
var pool = stub
var opponentCards = opponents
for _ in 0..<trials {
// Partial Fisher-Yates: only the first `drawCount` slots need to be random.
for i in 0..<drawCount {
let j = i + Int(rng.next() % UInt64(pool.count - i))
pool.swapAt(i, j)
}
var next = 0
var fullBoard = board
for _ in 0..<boardNeeded { fullBoard.append(pool[next]); next += 1 }
for seat in unknownSeats {
opponentCards[seat] = [pool[next], pool[next + 1]]
next += 2
}
let heroRank = HandEvaluator.evaluate(hero + fullBoard)
var bestOpponent = HandRank(value: 0)
var tiedWithHero = 0
for o in opponentCards {
let r = HandEvaluator.evaluate(o + fullBoard)
if r > bestOpponent { bestOpponent = r; tiedWithHero = 0 }
if r == heroRank { tiedWithHero += 1 }
}
if heroRank > bestOpponent {
wins += 1
} else if heroRank == bestOpponent {
ties += 1.0 / Double(tiedWithHero + 1)
losses += Double(tiedWithHero) / Double(tiedWithHero + 1)
} else {
losses += 1
}
}
let t = Double(trials)
return EquityResult(win: wins / t, tie: ties / t, lose: losses / t, trials: trials)
}
/// Equity against `count` opponents holding unknown cards.
public static func equity(
hero: [Card],
opponentCount: Int,
board: [Card] = [],
trials: Int = 10_000,
rng: inout SeededRandom
) -> EquityResult {
equity(hero: hero, opponents: Array(repeating: [], count: opponentCount), board: board, trials: trials, rng: &rng)
}
}