← poker-tutor
import Foundation
import PokerCore
/// A placeholder decision-maker so the engine can be watched playing something that looks like
/// poker. The real opponents — personalities, skill dial, adjusting to the player — are phase 2.
struct SimpleActor {
var rng: SeededRandom
mutating func act(_ legal: LegalActions, _ engine: HandEngine) -> PlayerAction {
let seat = engine.seats[legal.seat]
let opponents = max(1, engine.seats.filter { $0.isInHand && $0.seat != legal.seat }.count)
let equity = EquityCalculator.equity(
hero: seat.hole,
opponentCount: opponents,
board: engine.board,
trials: 400,
rng: &rng
).equity
// Roughly the share of the pot needed to make calling break even.
let potOdds = legal.callCost > 0 ? Double(legal.callCost) / Double(engine.pot + legal.callCost) : 0
if equity > 0.72, legal.canBetOrRaise {
let target = min(legal.maxRaiseTo, max(legal.minRaiseTo, engine.pot * 2 / 3))
return legal.canBet ? .bet(to: target) : .raise(to: target)
}
if legal.canCheck { return .check }
if equity > potOdds + 0.04 { return .call }
return .fold
}
}
func describe(_ engine: HandEngine, seatNames: [Int: String]) -> String {
let positions = engine.positions()
var lines: [String] = []
for seat in engine.seats where seat.status != .sittingOut {
let pos = positions[seat.seat].map { " (\($0.rawValue))" } ?? ""
let mark = engine.actor == seat.seat ? "→" : " "
let state = seat.status == .folded ? "folded" : (seat.status == .allIn ? "all in" : "\(seat.stack)")
lines.append(" \(mark) \(seatNames[seat.seat] ?? "P\(seat.seat)")\(pos) \(seat.hole.shorthand) \(state)")
}
return lines.joined(separator: "\n")
}
func playHand(_ engine: HandEngine, actor: inout SimpleActor, verbose: Bool, seatNames: [Int: String]) {
var lastStreet: Street? = nil
while !engine.isComplete, let legal = engine.legalActions() {
if verbose, lastStreet != engine.street {
lastStreet = engine.street
let board = engine.board.isEmpty ? "" : " board \(engine.board.shorthand)"
print(" — \(engine.street.name), pot \(engine.pot)\(board)")
}
let action = actor.act(legal, engine)
let name = seatNames[legal.seat] ?? "P\(legal.seat)"
try! engine.apply(action)
if verbose { print(" \(name) \(action)") }
}
}
func runSingleHand(seed: UInt64, players: Int) {
let seats = (0..<players).map { SeatState(seat: $0, playerID: "P\($0)", stack: 10_000) }
let names = Dictionary(uniqueKeysWithValues: seats.map { ($0.seat, $0.playerID) })
let config = HandConfig(level: BlindLevel(smallBlind: 50, bigBlind: 100, ante: .bigBlindAnte(100)),
buttonSeat: 0, seed: seed)
let engine = HandEngine(seats: seats, config: config)
print("Hand seed \(seed) — \(players) players, blinds 50/100")
print(describe(engine, seatNames: names))
print("")
var actor = SimpleActor(rng: SeededRandom(seed: seed &* 31))
playHand(engine, actor: &actor, verbose: true, seatNames: names)
let result = engine.result!
print("")
print(" board \(result.board.shorthand)")
for pot in result.pots {
print(" pot \(pot.amount)\(pot.isSide ? " (side)" : "") contested by \(pot.eligible.map { names[$0] ?? "" }.joined(separator: ", "))")
}
for award in result.awards {
let hand = award.hand.map { " with \($0)" } ?? " (everyone folded)"
print(" \(names[award.seat] ?? "") wins \(award.amount)\(hand)")
}
}
func runTournament(players: Int, seed: UInt64, verbose: Bool) {
let structure = TournamentStructure.standard(startingStack: 10_000, handsPerLevel: 15, placesPaid: 3, entrants: players)
let tourney = Tournament(structure: structure, playerIDs: (1...players).map { "Player \($0)" }, seed: seed)
var actor = SimpleActor(rng: SeededRandom(seed: seed &* 7 &+ 1))
var hands = 0
var lastLevel = -1
print("Tournament — \(players) players, \(structure.startingStack) chips each, \(structure.placesPaid) paid")
print("")
while let (seats, config) = tourney.nextHandSeats() {
let names = Dictionary(uniqueKeysWithValues: seats.map { ($0.seat, $0.playerID) })
let engine = HandEngine(seats: seats, config: config)
let levelIndex = min(tourney.handNumber / structure.handsPerLevel, structure.levels.count - 1)
if levelIndex != lastLevel {
lastLevel = levelIndex
let level = tourney.currentLevel
print("Level \(levelIndex + 1): blinds \(level.smallBlind)/\(level.bigBlind) — \(tourney.alive.count) left")
}
if verbose {
print("")
print("Hand \(hands + 1)")
print(describe(engine, seatNames: names))
}
playHand(engine, actor: &actor, verbose: verbose)
let before = Dictionary(uniqueKeysWithValues: seats.map { ($0.playerID, $0.stack) })
tourney.recordHand(finalStacks: engine.result!.finalStacks, seatIDs: names)
for player in tourney.players where player.finishedPlace != nil && (before[player.id] ?? 0) > 0 && player.stack == 0 {
print(" \(player.id) busts in \(ordinal(player.finishedPlace!)) place (hand \(hands + 1))")
}
hands += 1
if hands > 10_000 { print("stopping: tournament ran long"); break }
}
print("")
print("Finished after \(hands) hands.")
let payouts = tourney.payouts()
for player in tourney.players.sorted(by: { ($0.finishedPlace ?? 99) < ($1.finishedPlace ?? 99) }).prefix(structure.placesPaid) {
let share = payouts[player.id].map { String(format: "%.0f%% of the prize pool", $0 * 100) } ?? ""
print(" \(ordinal(player.finishedPlace ?? 0)): \(player.id) \(share)")
}
}
private func playHand(_ engine: HandEngine, actor: inout SimpleActor, verbose: Bool) {
let names = Dictionary(uniqueKeysWithValues: engine.seats.map { ($0.seat, $0.playerID) })
playHand(engine, actor: &actor, verbose: verbose, seatNames: names)
}
func ordinal(_ n: Int) -> String {
switch n {
case 1: return "1st"
case 2: return "2nd"
case 3: return "3rd"
default: return "\(n)th"
}
}