← poker-tutor
import PokerCore
import TestKit
func tournamentTests(_ t: TestRun) {
t.suite("tournament structure") { t in
t.test("blinds go up on schedule") {
let s = TournamentStructure.standard(startingStack: 10_000, handsPerLevel: 10)
t.equal(s.level(atHand: 0).bigBlind, s.levels[0].bigBlind)
t.equal(s.level(atHand: 9).bigBlind, s.levels[0].bigBlind)
t.equal(s.level(atHand: 10).bigBlind, s.levels[1].bigBlind)
t.greaterThan(s.levels[1].bigBlind, s.levels[0].bigBlind)
t.greaterThan(s.levels[5].bigBlind, s.levels[4].bigBlind)
}
t.test("blinds never go backwards and stay round") {
let s = TournamentStructure.standard()
for (a, b) in zip(s.levels, s.levels.dropFirst()) {
t.greaterThan(b.bigBlind, a.bigBlind)
t.expect(b.smallBlind * 2 <= b.bigBlind + 1, "small blind should be about half")
}
}
t.test("the schedule runs out of levels gracefully") {
let s = TournamentStructure.standard(handsPerLevel: 5)
t.equal(s.level(atHand: 100_000).bigBlind, s.levels.last!.bigBlind)
}
t.test("payouts sum to the whole prize pool") {
for places in 1...9 {
let ladder = TournamentStructure.payoutLadder(places: places)
t.close(ladder.reduce(0, +), 1.0, 0.0001, "\(places) places")
for (a, b) in zip(ladder, ladder.dropFirst()) {
t.greaterThan(a, b - 0.0001, "higher finishes pay more")
}
}
}
}
t.suite("prize-money equity") { t in
t.test("equal stacks split the prize pool equally") {
let e = ICM.equity(stacks: [1000, 1000, 1000], payouts: [0.5, 0.3, 0.2])
for share in e { t.close(share, 1.0 / 3.0, 0.001) }
}
t.test("everyone's equity sums to the prize pool") {
let e = ICM.equity(stacks: [5000, 3000, 1500, 500], payouts: [0.5, 0.3, 0.2])
t.close(e.reduce(0, +), 1.0, 0.001)
}
t.test("a bigger stack is worth more, but not proportionally") {
let e = ICM.equity(stacks: [8000, 1000, 1000], payouts: [0.5, 0.3, 0.2])
t.greaterThan(e[0], e[1])
let chipShare = 8000.0 / 10000.0
t.lessThan(e[0], chipShare, "doubling chips does not double prize equity")
t.greaterThan(e[0], 0.4, "but the chip leader is clearly ahead")
}
t.test("chips are worth less as the ladder flattens") {
// With winner-take-all, prize equity tracks chip share exactly.
let winnerTakeAll = ICM.equity(stacks: [8000, 1000, 1000], payouts: [1.0])
t.close(winnerTakeAll[0], 0.8, 0.001)
// Spread the money around and the leader's edge shrinks.
let spread = ICM.equity(stacks: [8000, 1000, 1000], payouts: [0.4, 0.35, 0.25])
t.lessThan(spread[0], winnerTakeAll[0])
}
t.test("a player with no chips is worth nothing") {
let e = ICM.equity(stacks: [5000, 5000, 0], payouts: [0.5, 0.3, 0.2])
t.close(e[2], 0.0, 0.001)
}
}
t.suite("running a tournament") { t in
t.test("a nine-handed tournament plays down to one winner") {
let structure = TournamentStructure.standard(startingStack: 3_000, handsPerLevel: 8, placesPaid: 3)
let tourney = Tournament(structure: structure, playerIDs: (0..<9).map { "P\($0)" }, seed: 77)
let totalChips = 9 * 3_000
var hands = 0
while let (seats, config) = tourney.nextHandSeats() {
let e = HandEngine(seats: seats, config: config)
var steps = 0
var rng = SeededRandom(seed: config.seed &+ 1)
while !e.isComplete, let legal = e.legalActions() {
let roll = rng.next() % 100
let action: PlayerAction
if roll < 45 && !legal.canCheck {
action = .fold
} else if roll < 90 {
action = legal.canCheck ? .check : .call
} else if legal.canBetOrRaise {
action = legal.canBet ? .bet(to: legal.minRaiseTo) : .raise(to: legal.minRaiseTo)
} else {
action = legal.canCheck ? .check : .call
}
try! e.apply(action)
steps += 1
precondition(steps < 400, "hand failed to terminate")
}
let ids = Dictionary(uniqueKeysWithValues: seats.map { ($0.seat, $0.playerID) })
tourney.recordHand(finalStacks: e.result!.finalStacks, seatIDs: ids)
let inPlay = tourney.players.reduce(0) { $0 + $1.stack }
t.equal(inPlay, totalChips, "chips left the tournament on hand \(hands)")
hands += 1
precondition(hands < 5_000, "tournament failed to finish")
}
t.expect(tourney.isComplete, "the tournament should be over")
t.equal(tourney.alive.count, 1)
let places = tourney.players.compactMap(\.finishedPlace).sorted()
t.equal(places, Array(1...9), "every player got a distinct finishing place")
t.close(tourney.payouts().values.reduce(0, +), 1.0, 0.0001, "the whole prize pool is paid out")
t.greaterThan(hands, 20, "a tournament should take more than a handful of hands")
}
t.test("the same seed replays the same tournament") {
func run() -> [String: Int] {
let structure = TournamentStructure.standard(startingStack: 2_000, handsPerLevel: 6)
let tourney = Tournament(structure: structure, playerIDs: (0..<6).map { "P\($0)" }, seed: 4242)
var hands = 0
while let (seats, config) = tourney.nextHandSeats() {
let e = HandEngine(seats: seats, config: config)
while !e.isComplete, let legal = e.legalActions() {
try! e.apply(legal.canCheck ? .check : .call)
}
let ids = Dictionary(uniqueKeysWithValues: seats.map { ($0.seat, $0.playerID) })
tourney.recordHand(finalStacks: e.result!.finalStacks, seatIDs: ids)
hands += 1
// A guard, not a formality: dead money handled wrongly once made this loop
// forever, with two short stacks refunded their own blinds every hand.
if hands > 3_000 {
t.expect(false, "tournament never ended — stacks \(tourney.alive.map { "\($0.id)=\($0.stack)" })")
break
}
}
return Dictionary(uniqueKeysWithValues: tourney.players.map { ($0.id, $0.finishedPlace ?? 0) })
}
t.equal(run(), run())
}
t.test("prize equity is available while the tournament is running") {
let structure = TournamentStructure.standard(startingStack: 5_000, placesPaid: 3)
let tourney = Tournament(structure: structure, playerIDs: ["a", "b", "c", "d"], seed: 9)
let equity = tourney.currentEquity()
t.equal(equity.count, 4)
t.close(equity.values.reduce(0, +), 1.0, 0.001)
}
}
}