← poker-tutor
import Foundation
import PokerCore
import PokerAI
/// Plays one matchup and reports where the money actually goes, so a losing bot can be diagnosed
/// instead of guessed at.
func runDiagnose(hands: Int, seed: UInt64, stack: Int, samples: Int) {
let a = Bot(id: "reg", personality: .solidRegular, skill: 90, seed: 1)
let b = Bot(id: "maniac", personality: .maniac, skill: 15, seed: 2)
let level = BlindLevel(smallBlind: 50, bigBlind: 100)
let models = OpponentModels()
var rng = SeededRandom(seed: seed &* 31 &+ 7)
var net = 0
var netByStreet: [Street: Int] = [:]
var potsByStreet: [Street: Int] = [:]
var shown = 0
for hand in 0..<hands {
let deckSeed = rng.next()
let seatSeeds = [rng.next(), rng.next()]
let button = hand % 2
for pass in 0..<2 {
let heroSeat = pass == 0 ? 0 : 1
let ids = pass == 0 ? [a.id, b.id] : [b.id, a.id]
let lineup = [
Bot(id: ids[0], personality: ids[0] == a.id ? a.personality : b.personality,
skill: ids[0] == a.id ? a.skill : b.skill, seed: seatSeeds[0]),
Bot(id: ids[1], personality: ids[1] == a.id ? a.personality : b.personality,
skill: ids[1] == a.id ? a.skill : b.skill, seed: seatSeeds[1]),
]
let states = ids.enumerated().map { SeatState(seat: $0.offset, playerID: $0.element, stack: stack) }
let engine = HandEngine(seats: states,
config: HandConfig(level: level, buttonSeat: button, seed: deckSeed))
BotTable(bots: lineup, seed: deckSeed, sharedModels: models).play(engine)
let result = engine.result!
let delta = result.net[heroSeat] ?? 0
net += delta
// Attribute the hand to the street on which it ended.
netByStreet[engine.street, default: 0] += delta
potsByStreet[engine.street, default: 0] += 1
if hand > 60 && shown < samples && pass == 0 && abs(delta) > stack / 4 {
shown += 1
print("--- hand \(hand), reg \(delta > 0 ? "won" : "lost") \(abs(delta)) ---")
print(" reg \(engine.seats[0].hole.shorthand)")
print(" maniac \(engine.seats[1].hole.shorthand)")
print(" board \(result.board.shorthand)")
for action in engine.actions {
let who = engine.seats[action.seat].playerID
let forced = action.isForced ? " (blind)" : ""
print(" \(action.street.name.padding(toLength: 8, withPad: " ", startingAt: 0)) \(who.padding(toLength: 7, withPad: " ", startingAt: 0)) \(action.action)\(forced)")
}
print("")
}
}
}
print("Reg vs maniac, \(hands * 2) hands at \(stack / level.bigBlind) big blinds deep")
print(String(format: " net %+d chips (%+.2f bb/100)", net, Double(net) / Double(level.bigBlind) / Double(hands * 2) * 100))
print("")
print("Where hands ended, and what the reg made there:")
for street in [Street.preflop, .flop, .turn, .river] {
let count = potsByStreet[street] ?? 0
guard count > 0 else { continue }
let chips = netByStreet[street] ?? 0
print(String(format: " %-8@ %5d hands %+8d chips (%+.2f bb per hand)",
street.name as NSString, count, chips,
Double(chips) / Double(level.bigBlind) / Double(count)))
}
print("")
for id in [a.id, b.id] {
let s = models[id]
func pct(_ v: Double?) -> String { v.map { String(format: "%.0f%%", $0 * 100) } ?? "—" }
print(" \(id.padding(toLength: 8, withPad: " ", startingAt: 0)) hands \(s.handsDealt) vpip \(pct(s.vpip)) pfr \(pct(s.pfr)) 3bet \(pct(s.threeBetRate)) fold-to-bet \(pct(s.foldToBet)) aggression \(s.aggressionFactor.map { String(format: "%.1f", $0) } ?? "—")")
}
}