← poker-tutor
import Foundation
import PokerCore
import PokerAI
/// Measures opponents against each other so the skill dial can be checked by eye as well as
/// by the test suite.
func runShootout(hands: Int, seed: UInt64, stack: Int = 10_000, structure: BettingStructure = .noLimit) {
print("Duplicate matches — each hand dealt to both players, so the cards cancel out. \(structure.name).")
print("Positive means the first player wins, in big blinds per 100 hands.")
print("")
func match(_ label: String, _ a: Bot, _ b: Bot) {
let result = Shootout.duplicate(a, b, hands: hands, seed: seed, startingStack: stack, structure: structure)
let bar = String(repeating: "█", count: min(40, Int(abs(result.bigBlindsPer100) / 2)))
print(String(format: " %-46@ %+8.2f %@", label as NSString, result.bigBlindsPer100, bar))
}
print("Style against style")
match("Solid regular (90) vs calling station (5)",
Bot(id: "a", personality: .solidRegular, skill: 90, seed: 1),
Bot(id: "b", personality: .callingStation, skill: 5, seed: 2))
match("Solid regular (90) vs nit (20)",
Bot(id: "a", personality: .solidRegular, skill: 90, seed: 3),
Bot(id: "b", personality: .nit, skill: 20, seed: 4))
match("Solid regular (90) vs maniac (15)",
Bot(id: "a", personality: .solidRegular, skill: 90, seed: 5),
Bot(id: "b", personality: .maniac, skill: 15, seed: 6))
match("Tight aggressive (70) vs loose passive (15)",
Bot(id: "a", personality: .tightAggressive, skill: 70, seed: 7),
Bot(id: "b", personality: .loosePassive, skill: 15, seed: 8))
print("")
print("Same style, different skill — this is the dial that has to be real")
for personality in [Personality.tightAggressive, .solidRegular, .looseAggressive, .nit] {
let band = personality.skillRange
let expert = Bot(id: "a", personality: personality, skill: band.upperBound, seed: 11)
let novice = Bot(id: "b", personality: personality, skill: band.lowerBound, seed: 12)
match("\(personality.name) \(band.upperBound) vs \(band.lowerBound)", expert, novice)
}
print("")
print("Six-handed, same style at two skill levels — the dial in the format the app uses")
for personality in [Personality.tightAggressive, .solidRegular, .looseAggressive, .nit, .callingStation] {
let band = personality.skillRange
let expert = Bot(id: "hero", personality: personality, skill: band.upperBound, seed: 11)
let r = Shootout.ring(expert, versus: personality, villainSkill: band.lowerBound, seats: 6,
hands: max(200, hands / 6), seed: seed, startingStack: stack, structure: structure)
print(String(format: " %-24@ %3d vs %3d %+8.2f bb/100", personality.name as NSString,
band.upperBound, band.lowerBound, r.bigBlindsPer100))
}
print("")
print("Six-handed, one style against a table of another")
for (label, style, skill) in [("calling station", Personality.callingStation, 15),
("loose passive", Personality.loosePassive, 15),
("maniac", Personality.maniac, 15),
("nit", Personality.nit, 25),
("tight aggressive", Personality.tightAggressive, 55)] {
let hero = Bot(id: "hero", personality: .solidRegular, skill: 90, seed: 31)
let r = Shootout.ring(hero, versus: style, villainSkill: skill, seats: 6, hands: max(200, hands / 6), seed: seed, startingStack: stack, structure: structure)
print(String(format: " solid regular (90) vs a table of %-18@ %+8.2f bb/100", label as NSString, r.bigBlindsPer100))
}
print("")
print("Climbing the ladder — one strong player against tables of rising difficulty")
let hero = Bot(id: "hero", personality: .solidRegular, skill: 95, seed: 21)
for difficulty in [10, 30, 50, 70, 90] {
var rng = SeededRandom(seed: 500 &+ UInt64(difficulty))
let lineup = TableComposition.lineup(difficulty: difficulty, opponents: 5, rng: &rng)
let villains = lineup.enumerated().map {
Bot(id: "V\($0.offset)", personality: $0.element.personality, skill: $0.element.skill,
seed: 900 &+ UInt64($0.offset))
}
let result = ringMatch(hero: hero, villains: villains, hands: hands, seed: seed &+ UInt64(difficulty), structure: structure)
let names = Set(lineup.map { $0.personality.name }).sorted().joined(separator: ", ")
print(String(format: " difficulty %3d %+8.2f bb/100 table: %@", difficulty, result, names as NSString))
}
}
private func ringMatch(hero: Bot, villains: [Bot], hands: Int, seed: UInt64, structure: BettingStructure = .noLimit) -> Double {
let level = BlindLevel(smallBlind: 50, bigBlind: 100)
let all = [hero] + villains
let table = BotTable(bots: all, seed: seed)
var rng = SeededRandom(seed: seed &* 13 &+ 5)
var net = 0
for hand in 0..<hands {
let seats = all.enumerated().map { SeatState(seat: $0.offset, playerID: $0.element.id, stack: 10_000) }
let config = HandConfig(level: level, buttonSeat: hand % all.count, seed: rng.next(), structure: structure)
let engine = HandEngine(seats: seats, config: config)
table.play(engine)
net += engine.result?.net[0] ?? 0
}
return Double(net) / Double(level.bigBlind) / Double(hands) * 100
}