← poker-tutor
import Foundation
import PokerCore
import PokerCoach
import PokerSession
import PokerTests
/// Where the seat diagnosis's bars come from.
///
/// pokerlab seat-sweep --player control --hands 6000 --seeds 4
/// pokerlab seat-sweep --player badseat4 --hands 3000 --seeds 6
///
/// Runs a player through the app's own nine-handed tournament table and, at each checkpoint of
/// hands, prints every seat's cost per hundred and whether `SeatDiagnosis` would name a worst
/// seat. The control must never be named; the planted player must be named at the seat it was
/// planted in, with its habit leading. `minimumHands` and `requiredMargin` are read off these two
/// runs: the control's widest worst-vs-second gap at a checkpoint is the floor the margin has to
/// clear, and the checkpoint at which the planted seat is found on most seeds is the sample.
func runSeatSweep(playerName: String, hands: Int, seedCount: Int, firstSeed: UInt64, rung: Int) {
let player = plantedPlayer(named: playerName)
let checkpoints = [900, 1_350, 2_250, 3_600, 6_300].filter { $0 <= hands } + [hands]
print("Seat sweep: \(player.name), \(seedCount) seeds × \(hands) hands, rung \(rung)")
print("cells = seat:mistake-rate%/decisions/hands; gap = worst seat's rate minus the next, in points\n")
var namedAt: [Int: Int] = [:]
var gaps: [Int: [Double]] = [:]
for i in 0..<seedCount {
let seed = firstSeed &+ UInt64(i) &* 977
let session = runPlantedTournament(player, hands: hands, seed: seed, rung: rung)
let all = session.history.hands(in: .allTime).sorted { $0.playedAt < $1.playedAt }
print("seed \(seed)")
for point in Array(Set(checkpoints)).sorted() {
let diagnosis = SeatDiagnosis(of: Array(all.prefix(point)))
let sorted = diagnosis.seats.filter { $0.decisions >= SeatDiagnosis.minimumDecisions / 2 }
.sorted { $0.mistakeRate > $1.mistakeRate }
let cells = diagnosis.seats.map { seat in
return "\(seat.position):\(Int((seat.mistakeRate * 100).rounded()))%/\(seat.decisions)d/\(seat.hands)h"
}.joined(separator: " ")
let gap = sorted.count > 1 ? (sorted[0].mistakeRate - sorted[1].mistakeRate) * 100 : 0
gaps[point, default: []].append(gap)
let verdict: String
if let worst = diagnosis.worst {
namedAt[point, default: 0] += 1
let leading = worst.leading.first.map { "\($0.leak.id)×\($0.times)" } ?? "—"
verdict = "NAMED \(worst.seatPhrase) (\(leading))"
} else {
verdict = "not named"
}
print(" \(point) hands gap \(String(format: "%5.1f", gap)) \(verdict)")
print(" " + cells)
}
}
print("\nsummary (seat named / seeds; widest worst-vs-next gap):")
for point in Array(Set(checkpoints)).sorted() {
let widest = gaps[point]?.max() ?? 0
let mean = (gaps[point] ?? []).reduce(0, +) / Double(max(1, gaps[point]?.count ?? 1))
print(String(format: " %5d hands: named %d/%d, gap widest %.1f mean %.1f",
point, namedAt[point] ?? 0, seedCount, widest, mean))
}
}