← poker-tutor
import Foundation
import PokerCore
import PokerAI
import PokerCoach
import PokerSession
import TestKit
/// Plays a cash session hand by hand, choosing the player's action each time.
private func playCash(_ session: TableSession, hands: Int, choose: (LegalActions, TableSession) -> PlayerAction) {
var guardRail = 0
while session.isPlaying, session.handNumber <= hands {
guardRail += 1
precondition(guardRail < 60_000, "session failed to progress")
if let legal = session.legal {
session.act(choose(legal, session))
} else if session.engine?.isComplete == true {
if session.handNumber >= hands { return }
session.continueAfterReview()
} else if session.opponentToAct {
session.stepOpponent()
} else {
return
}
}
}
private func cashSession(seed: UInt64 = 7, setup: CashSetup = .standard,
history: HistoryStore = .inMemory()) -> TableSession {
let session = TableSession(seed: seed, history: history, variant: .pokerTutorCash)
session.cashSetup = setup
return session
}
func cashTableTests(_ t: TestRun) {
t.suite("a cash table") { t in
t.test("everybody sits down with a hundred big blinds at the chosen stakes, and nothing else moves") {
let setup = CashSetup(stakes: BlindLevel(smallBlind: 100, bigBlind: 200), structure: .noLimit)
let session = cashSession(setup: setup)
session.start(.cash)
t.equal(session.mode, .cash)
t.equal(session.level, setup.stakes)
t.equal(session.level.ante, AnteStyle.none, "no antes at a cash table")
for seat in session.engine!.seats {
t.equal(seat.stack + seat.committedThisRound + seat.antePaid, 20_000, "\(seat.playerID) sat down with 100 big blinds")
}
playCash(session, hands: 30) { legal, _ in legal.canCheck ? .check : .fold }
t.equal(session.level, setup.stakes, "the blinds are where they were set thirty hands later")
t.equal(session.engine!.config.level.ante, AnteStyle.none)
t.greaterThan(session.handNumber, 29)
}
t.test("the table plays the structure the setup asked for") {
for structure in BettingStructure.allCases {
let session = cashSession(setup: CashSetup(stakes: CashSetup.stakesOnOffer[1], structure: structure))
session.start(.cash)
t.equal(session.engine?.config.structure, structure)
t.equal(session.bettingStructure, structure)
playCash(session, hands: 3) { legal, _ in legal.canCheck ? .check : .call }
t.equal(session.history.hands.last?.structure, structure.rawValue, "the hand says how it was limited")
}
}
t.test("stacks carry from hand to hand instead of being topped back up") {
let session = cashSession()
session.start(.cash)
// Fold everything: the blinds drain the player's stack a little at a time.
playCash(session, hands: 40) { legal, _ in legal.canCheck ? .check : .fold }
let hero = session.seat(session.heroSeat)!
t.lessThan(hero.stack + hero.committedThisRound, session.cashSetup.buyIn,
"forty hands of blinds have come off the stack and stayed off")
t.equal(session.rebuys["You"] ?? 0, 0, "nobody rebuys while they still have chips")
}
t.test("a player who busts buys back in for a hundred big blinds, and the rebuy is counted") {
let session = cashSession(seed: 3)
session.start(.cash)
// Move in every hand: the player busts sooner or later, and so do some opponents.
playCash(session, hands: 400) { legal, _ in
legal.canBetOrRaise ? (legal.canBet ? .bet(to: legal.maxRaiseTo) : .raise(to: legal.maxRaiseTo))
: (legal.canCall ? .call : .check)
}
let mine = session.rebuys["You"] ?? 0
t.greaterThan(mine, 0, "moving in for 400 hands busts at least once")
// The buy-back happens as the next hand is dealt, so deal it.
if session.engine?.isComplete == true { session.continueAfterReview() }
let hero = session.seat(session.heroSeat)!
t.greaterThan(hero.stack + hero.committedThisRound, 0, "and the player is still at the table with chips")
// The hand after each bust starts with exactly the buy-in in front of the player —
// a hundred big blinds, not a top-up to whatever was lost.
let hands = session.history.hands
var rebuysSeen = 0
for hand in hands {
let now = hand.rebuys ?? 0
if now > rebuysSeen, let me = hand.players?.first(where: { $0.isHero }) {
t.equal(me.startingStack, session.cashSetup.buyIn,
"hand \(hand.handNumber) after a bust should start with the buy-in")
}
rebuysSeen = max(rebuysSeen, now)
}
t.equal(session.history.hands.last?.rebuys, mine, "the hand carries the count")
let recorded = session.history.hands.map { $0.rebuys ?? 0 }
t.expect(recorded == recorded.sorted(), "the count written down never goes down")
}
t.test("an opponent who busts twice leaves, and somebody new sits down") {
let session = cashSession(seed: 3)
session.start(.cash)
let original = Set(session.seatedIDs)
playCash(session, hands: 400) { legal, _ in
legal.canBetOrRaise ? (legal.canBet ? .bet(to: legal.maxRaiseTo) : .raise(to: legal.maxRaiseTo))
: (legal.canCall ? .call : .check)
}
let now = Set(session.seatedIDs)
t.equal(now.count, original.count, "the table stays full")
t.expect(now != original, "at least one seat changed hands over 400 all-in hands")
let newcomers = now.subtracting(original)
for name in newcomers {
t.expect(session.opponents[name] != nil, "\(name) is somebody the coach can read")
t.expect(!original.contains(name), "a new name, not a returning one")
}
t.expect(now.contains("You"), "the player never leaves this way")
}
t.test("cashing out reports what the hands say, and nothing the hands do not") {
let history = HistoryStore.inMemory()
let session = cashSession(seed: 5, setup: CashSetup(stakes: CashSetup.stakesOnOffer[0], structure: .potLimit),
history: history)
session.start(.cash)
playCash(session, hands: 25) { legal, _ in legal.canCheck ? .check : .call }
let played = history.hands.filter { $0.sessionID == session.sessionID }
session.leave()
let result = session.lastCashResult!
t.equal(result.hands, played.count)
t.equal(result.netChips, played.reduce(0) { $0 + $1.netChips })
t.close(result.netBB, played.reduce(0.0) { $0 + $1.netBB }, 0.001)
t.equal(result.setup.structure, .potLimit)
t.close(result.bbPer100, result.netBB / Double(result.hands) * 100, 0.001)
t.expect(!session.isPlaying)
}
t.test("a cash table never judges a stack as a push-or-fold spot") {
// Twelve big blinds at a cash table is somebody who has lost some of a buy-in, not
// somebody the blinds are about to swallow. Until Phase 2 says what to say, the
// coach says nothing about it — and a tournament table still does.
let cash = cashSession()
t.expect(!cash.coach.judgesShortStacks)
let tournament = TableSession(seed: 7, history: .inMemory(), variant: .pokerTutor)
t.expect(tournament.coach.judgesShortStacks)
}
t.test("a tournament hand is written down exactly as before") {
let session = TableSession(seed: 9, history: .inMemory())
session.start(.tournament)
playCash(session, hands: 3) { legal, _ in legal.canCheck ? .check : .fold }
let hand = session.history.hands.last!
t.equal(hand.structure, nil, "no structure field on a tournament hand")
t.equal(hand.rebuys, nil)
t.equal(session.engine?.config.structure ?? .noLimit, .noLimit)
}
}
}