← poker-tutor
import Foundation
import PokerCore
import PokerAI
import PokerCoach
import PokerSession
import TestKit
/// Plays a whole assessment headlessly, driving `TableSession` exactly as a person would: by
/// answering when it is the player's turn and asking for the next hand when the last one is over.
///
/// Nothing about the assessment lives in the app layer, which is what makes this possible — and
/// what makes it the test that matters, because the same calls are what the screen makes.
public func playAssessment(_ player: PlantedPlayer, seed: UInt64,
hands: Int = AssessmentRun.standardLength,
history: HistoryStore = .inMemory(),
pinned: AssessmentSetup? = nil,
setup: CashSetup? = nil) -> TableSession {
let session = TableSession(seed: seed, history: history, variant: AppVariant.current)
session.pinnedSetup = pinned
// A cash product's assessment is played at the stakes and structure the player chose.
if let setup { session.cashSetup = setup }
session.startAssessment(hands: hands)
driveAssessment(session, player: player, seed: seed)
return session
}
/// Answers for the player until the sitting stops. Split out so a test can stop half way, put the
/// session down, and pick a new one up off the same history.
public func driveAssessment(_ session: TableSession, player: PlantedPlayer, seed: UInt64,
stoppingAfter limit: Int = .max) {
var rng = SeededRandom(seed: seed &* 7 &+ 3)
var steps = 0
while session.isPlaying, (session.assessment?.handsPlayed ?? 0) < limit {
steps += 1
precondition(steps < 50_000, "the assessment did not terminate")
if session.heroToAct, let view = session.engine?.view(for: session.heroSeat) {
session.act(player.decide(view, &rng))
} else if session.engine?.isComplete == true {
session.continueAfterReview()
} else {
session.finishHandNow()
}
}
}
/// A coverage with everything measured to death except the spots named — the state the planner is
/// meant to react to.
func coverage(missing gaps: Set<SpotKind>) -> AssessmentCoverage {
var coverage = AssessmentCoverage()
for spot in SpotKind.allCases where !gaps.contains(spot) {
for index in 0..<400 { coverage.saw(spot, mistake: index % 6 == 0) }
}
return coverage
}
public func assessmentTests(_ t: TestRun) {
t.suite("what the assessment knows it does not know") { t in
t.test("a situation nobody has been watched in is worth nothing") {
let empty = AssessmentCoverage()
for spot in SpotKind.allCases {
t.equal(empty.certainty(in: spot), 0, "\(spot.name) started out already understood")
}
}
t.test("watching the same spot makes it surer, and never completely sure") {
// Deliberately not step by step. This is a standard error, so one more chance that
// pulls the rate back towards a coin flip really does make the measurement slightly
// less certain, and that is correct rather than a bug — it is the whole reason a
// muddled player takes longer to read than a consistent one. What has to hold is the
// direction over a run of hands.
var coverage = AssessmentCoverage()
var marks: [Int: Double] = [:]
for round in 0..<60 {
coverage.saw(.bigBlindDefence, mistake: round % 4 == 0)
marks[round + 1] = coverage.certainty(in: .bigBlindDefence)
t.lessThan(coverage.certainty(in: .bigBlindDefence), 1.0,
"nothing is ever completely known")
}
t.expect(marks[4]! > marks[1]!, "four chances told it no more than one")
t.expect(marks[20]! > marks[4]!, "twenty chances told it no more than four")
t.expect(marks[60]! > marks[20]!, "sixty chances told it no more than twenty")
t.expect(marks[60]! > 0.8, "sixty chances should firm a rate up; got \(marks[60]!)")
}
t.test("a spot that always goes the same way firms up faster than a coin flip") {
var settled = AssessmentCoverage()
var muddled = AssessmentCoverage()
for index in 0..<20 {
settled.saw(.facingRaise, mistake: false)
muddled.saw(.facingRaise, mistake: index % 2 == 0)
}
t.expect(settled.certainty(in: .facingRaise) > muddled.certainty(in: .facingRaise),
"a player who never gets this wrong is better understood than one who is "
+ "half and half on the same number of hands")
}
}
t.suite("choosing the next hand") { t in
// This is the adaptive part, tested where it can be pinned down: given a picture of what
// has and has not been seen, the planner has to go to the gap.
t.test("it goes to the situation nobody has looked at") {
let wanted: [SpotKind: String] = [
.shortStackShove: "short-stack",
.bigBlindDefence: "big-blind",
.facingReraise: "facing-heat",
.facingRiverBet: "bet-at-you",
.riverDecision: "deep-and-sticky",
]
for (spot, setup) in wanted {
let chosen = AssessmentPlanner.next(for: coverage(missing: [spot]))
t.equal(chosen.id, setup,
"with only \(spot.name) unmeasured it should set the table up for it")
}
}
t.test("the same picture always chooses the same hand") {
let picture = coverage(missing: [.turnDecision, .facingRaise])
let first = AssessmentPlanner.next(for: picture)
for _ in 0..<5 {
t.equal(AssessmentPlanner.next(for: picture).id, first.id)
}
}
t.test("every situation the coach measures is reachable") {
// A spot no setup ever aims at is one the assessment can never report on, which would
// make its silence about that part of the game meaningless rather than reassuring.
for spot in SpotKind.allCases {
guard SpotKind.measured.contains(spot) else { continue }
let reachable = AssessmentPlanner.setups.contains { ($0.expected[spot] ?? 0) > 0 }
t.expect(reachable, "no setup ever aims at \(spot.name)")
}
}
t.test("it stops asking a question it has the answer to") {
let known = coverage(missing: [])
let unknown = AssessmentCoverage()
for setup in AssessmentPlanner.setups {
t.lessThan(AssessmentPlanner.gain(of: setup, given: known),
AssessmentPlanner.gain(of: setup, given: unknown),
"\(setup.id) is worth as much when everything is known as when nothing is")
}
}
}
t.suite("an assessment sitting") { t in
t.test("the player sits where the planner asked, at the depth it asked for") {
let session = TableSession(seed: 31, history: .inMemory())
session.startAssessment(hands: 12)
var checked = 0
var seatsUsed: Set<Position> = []
var depths: Set<Int> = []
var rng = SeededRandom(seed: 99)
var steps = 0
while session.isPlaying {
steps += 1
precondition(steps < 50_000)
if let run = session.assessment, let engine = session.engine, engine.actions.count <= 9 {
let seat = engine.positions()[session.heroSeat]
t.equal(seat, run.setup.seat, "hand \(session.handNumber) sat the player wrong")
let stack = (engine.seats[session.heroSeat].stack
+ engine.seats[session.heroSeat].committedThisHand) / engine.config.level.bigBlind
t.equal(stack, run.setup.depthInBB, "hand \(session.handNumber) was the wrong depth")
seatsUsed.insert(run.setup.seat)
depths.insert(run.setup.depthInBB)
checked += 1
}
if session.heroToAct, let view = session.engine?.view(for: session.heroSeat) {
session.act(PlantedPlayer.control.decide(view, &rng))
} else if session.engine?.isComplete == true {
session.continueAfterReview()
} else {
session.finishHandNow()
}
}
t.expect(checked > 0, "never got a look at a hand in progress")
t.expect(seatsUsed.count >= 3, "twelve hands only ever used \(seatsUsed.count) seat(s)")
t.expect(depths.count >= 2, "the chip depth never changed")
}
t.test("it reaches more of the game than sitting still does") {
// The point of steering, measured rather than asserted: across several seeds, forty
// steered hands have to put the player in more different kinds of situation than forty
// hands of an ordinary table do. One seed proves nothing here — the first version of
// this measurement passed on luck and had to be rewritten, twice.
var steeredTotal = 0
var ordinaryTotal = 0
for seed: UInt64 in [7, 21, 44, 108, 255] {
let steered = playAssessment(.control, seed: seed).assessment?.coverage ?? AssessmentCoverage()
let plain = TableSession(seed: seed, history: .inMemory())
plain.start(.cash)
var rng = SeededRandom(seed: seed &* 7 &+ 3)
var cover = AssessmentCoverage()
var steps = 0
while plain.isPlaying, plain.handNumber <= AssessmentRun.standardLength {
steps += 1
precondition(steps < 50_000)
if plain.heroToAct, let view = plain.engine?.view(for: plain.heroSeat) {
plain.act(PlantedPlayer.control.decide(view, &rng))
} else if plain.engine?.isComplete == true {
for verdict in plain.history.hands.last?.verdicts ?? [] {
if let spot = verdict.spotKind { cover.saw(spot, mistake: verdict.countsTowardsLeak) }
}
plain.continueAfterReview()
} else {
plain.finishHandNow()
}
}
steeredTotal += SpotKind.allCases.filter { steered.chances(in: $0) >= 3 }.count
ordinaryTotal += SpotKind.allCases.filter { cover.chances(in: $0) >= 3 }.count
}
t.expect(steeredTotal > ordinaryTotal,
"steering reached \(steeredTotal) situations across five seeds against "
+ "\(ordinaryTotal) for an ordinary table — it is not earning its keep")
}
t.test("it stops when it said it would") {
let session = playAssessment(.control, seed: 5, hands: 14)
t.equal(session.assessment?.handsPlayed, 14)
t.expect(session.assessment?.isFinished == true)
t.expect(!session.isPlaying, "the sitting should be over")
t.equal(session.history.hands.count, 14)
}
t.test("a hand written down before assessments existed is not one") {
// The other half of the optional-field rule: an old line has to read, and it has to
// read as what it was. A missing field must not quietly become "yes".
let old = HandRecord(sessionID: "old", sessionStarted: Date(), playedAt: Date(),
handNumber: 1, mode: "Tournament", bigBlind: 100, position: "BTN",
holeCards: "Ah Kd", board: "", netChips: 0, wentToShowdown: false,
sawFlop: false, putMoneyInPreflop: false, raisedPreflop: false,
verdicts: [])
t.expect(!old.isAssessment, "a hand with no kind was taken for an assessment hand")
t.equal(GameAnalysis(of: [old]).hands, 1, "an ordinary hand was left out of the numbers")
t.equal(GameAnalysis(of: [old]).assessmentHands, 0)
}
t.test("every hand it deals says so on the way to disk") {
let session = playAssessment(.control, seed: 6, hands: 8)
t.equal(session.history.hands.count, 8, "no hands to check")
for hand in session.history.hands {
t.expect(hand.isAssessment, "hand \(hand.handNumber) was not marked as an assessment hand")
t.expect(hand.setup != nil, "hand \(hand.handNumber) did not record how it was set up")
t.expect(AssessmentPlanner.setup(id: hand.setup ?? "") != nil,
"hand \(hand.handNumber) recorded a setup nothing recognises")
}
}
}
t.suite("stopping an assessment half way") { t in
t.test("it picks back up exactly where it was left") {
let store = HistoryStore.inMemory()
let first = TableSession(seed: 77, history: store)
first.startAssessment(hands: 40)
driveAssessment(first, player: .foldsBigBlind, seed: 77, stoppingAfter: 15)
let stoppedAt = first.assessment?.handsPlayed ?? 0
let sitting = first.sessionID
t.expect(stoppedAt >= 15 && stoppedAt <= 16, "stopped at \(stoppedAt)")
// A new session, as a fresh launch would build one, reading the same history back.
let second = TableSession(seed: 900, history: store)
let resumed = second.resumeAssessment(sitting)
t.expect(resumed, "an unfinished sitting should be resumable")
t.equal(second.assessment?.handsPlayed, stoppedAt)
t.equal(second.assessment?.coverage, first.assessment?.coverage,
"the picture of what has been seen has to survive the reload")
t.equal(second.sessionID, sitting, "resuming must not start a new sitting")
// The fourth thing the sitting holds still. The eight opponents are the same eight
// either way, but the skill they play at was being recomputed off a rating that had
// moved in the meantime, so a sitting picked back up was measured against slightly
// better or worse people than it started against.
t.equal(second.tableDifficulty, first.tableDifficulty,
"the opponents changed skill part way through a measurement")
}
t.test("walking away leaves the history and the rating alone") {
let store = HistoryStore.inMemory()
let session = TableSession(seed: 12, history: store)
session.startAssessment(hands: 40)
driveAssessment(session, player: .control, seed: 12, stoppingAfter: 9)
session.leave()
let report = AssessmentReport(of: store.hands)
t.expect(!report.finished, "a sitting that was abandoned must not read as finished")
t.expect(report.headline.contains("stopped"), "the report has to admit it: \(report.headline)")
t.equal(LeakScorecard(of: store.hands).hands, store.hands.count,
"the hands played still count towards the leaks")
t.equal(GameAnalysis(of: store.hands).hands, 0,
"and none of them count towards a win rate")
}
t.test("a finished sitting is not offered again") {
let store = HistoryStore.inMemory()
let session = TableSession(seed: 4, history: store)
_ = playAssessment(.control, seed: 4, hands: 6, history: store)
t.equal(session.unfinishedAssessment(target: 6)?.id, nil)
t.expect(session.unfinishedAssessment(target: 40) != nil,
"six hands of a forty-hand sitting is unfinished")
}
t.test("one sitting walked out on does not hide the one that was finished") {
// Home shows the result and the way back to the report only while there is nothing to
// carry on with, and it used to look for the newest *unfinished* sitting rather than
// simply the newest one. So walking away three hands in, once, and then sitting a full
// assessment afterwards left the offer stuck on "Carry on" from the three-hand stub for
// ever, with no route to the report of the one that was actually finished. An
// assessment that has been superseded is history rather than an offer.
let store = HistoryStore.inMemory()
let abandoned = TableSession(seed: 31, history: store)
abandoned.startAssessment(hands: 6)
driveAssessment(abandoned, player: .control, seed: 31, stoppingAfter: 2)
abandoned.leave()
let stub = store.sessions().first?.id
let session = TableSession(seed: 4, history: store)
t.equal(session.unfinishedAssessment(target: 6)?.id, stub,
"while it is the newest one, it is the one to carry on with")
_ = playAssessment(.control, seed: 4, hands: 6, history: store)
t.equal(session.unfinishedAssessment(target: 6)?.id, nil,
"the stub is still being offered over a sitting that ran to the end")
t.expect(TableSession(seed: 5, history: store)
.lastFinishedAssessment(target: 6) != nil,
"and the finished one has to be reachable")
}
t.test("the sitting on the table is not offered as something to carry on with") {
// You are in it. Offering it back reads as though there were another one waiting, and
// it is the thing that hides the finished report behind it.
let store = HistoryStore.inMemory()
let session = TableSession(seed: 77, history: store)
session.startAssessment(hands: 40)
driveAssessment(session, player: .control, seed: 77, stoppingAfter: 4)
t.greaterThan(store.hands.count, 0, "no hands were played, so nothing was tested")
t.equal(session.unfinishedAssessment(target: 40)?.id, nil,
"it offered to carry on with the sitting already on the table")
// A fresh launch, which is not sitting at it, is offered it.
t.equal(TableSession(seed: 900, history: store)
.unfinishedAssessment(target: 40)?.id, session.sessionID)
}
}
t.suite("the assessment report") { t in
t.test("assessment hands stay out of the win rate and inside the leaks") {
let session = playAssessment(.foldsBigBlind, seed: 3, hands: 20)
let all = session.history.hands
let analysis = GameAnalysis(of: all)
t.equal(analysis.hands, 0, "an assessment must not be counted as hands played")
t.equal(analysis.assessmentHands, 20)
t.expect(analysis.arrangedNote != nil, "the screen has to be able to say why")
t.equal(LeakScorecard(of: all).hands, 20, "the decisions still count")
}
t.test("it gives a range rather than a number it cannot support") {
let session = playAssessment(.control, seed: 8)
guard let report = session.assessmentReport else { return t.expect(false, "no report") }
t.expect(report.ratingRange.contains(report.rating.value),
"the rating fell outside its own range")
t.expect(report.ratingRange.lowerBound <= report.ratingRange.upperBound)
t.expect(report.precisionNote.contains("\(report.hands) hands"),
"the note has to name the sample it is apologising for")
// Forty hands cannot reach eleven situations, so some of them always come back empty
// — and the assertion below is about exactly those. Counted first, because a loop over
// nothing passes without checking a single line.
let never = report.areas.filter { $0.chances == 0 }
t.greaterThan(never.count, 0,
"\(report.hands) hands reached every one of the \(report.areas.count) "
+ "situations, so nothing below was checked")
for area in never {
t.expect(area.line.contains("Never came up"),
"\(area.name) had nothing measured and did not say so")
}
}
t.test("it says what it steered") {
let session = playAssessment(.control, seed: 9, hands: 20)
guard let report = session.assessmentReport else { return t.expect(false, "no report") }
t.equal(report.steering.reduce(0) { $0 + $1.hands }, 20,
"every hand has to be accounted for in what was arranged")
t.expect(report.steering.count >= 3, "twenty hands used only \(report.steering.count) setups")
t.expect(report.fairnessNote.contains("never touched is the cards"),
"the one promise the whole measurement rests on has to be in the report")
}
t.test("what each hand was looking at is disclosed at the end, not during") {
// These hands count towards the leaks, so naming the habit being counted while it is
// being counted measures somebody who knows what is being watched — which is the same
// objection that keeps drill hands out of the scorecard altogether. The list is not
// suppressed, it is moved: it belongs in the report, where saying it costs nothing.
let session = playAssessment(.control, seed: 9, hands: 20)
guard let report = session.assessmentReport else { return t.expect(false, "no report") }
t.greaterThan(report.steering.count, 0, "nothing was arranged, so nothing was checked")
for steer in report.steering {
t.expect(!steer.setup.watching.isEmpty,
"\(steer.setup.id) was arranged and the report does not say what for")
}
t.expect(report.fairnessNote.contains("rather than while you played"),
"the report has to own the fact that it kept this back: "
+ "\"\(report.fairnessNote)\"")
// And the band above the felt says the seat and the stacks, which the player can see
// on the felt anyway, and nothing about which habit is being counted.
for setup in AssessmentPlanner.setups {
let banner = setup.introduction(atTableOf: 9)
t.expect(!banner.contains(setup.watching),
"the live banner names the habit it is measuring: \"\(banner)\"")
}
}
t.test("it finds a planted flaw and does not invent company for it") {
// The measurement that decides whether an assessment is worth sitting through. Five
// seeds, because forty hands of poker is a small sample and one seed here would be
// measuring luck — which this project has already been caught doing twice.
//
// The bars differ on purpose, and the difference is the honest part. Playing far too
// many hands shows up in every unopened pot, so forty hands proves it outright almost
// every time. Folding the big blind only shows up when somebody raises at it *and* the
// hand was worth continuing with, which is six to eleven chances in a sitting this
// long — enough to raise, not always enough to prove. Setting both bars at "always"
// would only be achievable by loosening what the report is allowed to claim.
let seeds: [UInt64] = [11, 202, 3003, 40004, 500005]
var proven = 0
var strangersProven = 0
for seed in seeds {
let report = playAssessment(.playsEverything, seed: seed).assessmentReport
if (report?.named ?? []).contains(where: { $0.leak.id == "too-many-hands" }) { proven += 1 }
strangersProven += (report?.named ?? []).filter { $0.leak.id != "too-many-hands" }.count
}
t.expect(proven >= 4,
"a player who plays far too many hands was only proved in \(proven) of "
+ "\(seeds.count) sittings")
t.lessThan(strangersProven, 2,
"\(strangersProven) habits were proved that were never planted")
var raised = 0
var strangers = 0
for seed in seeds {
let report = playAssessment(.foldsBigBlind, seed: seed).assessmentReport
let all = (report?.named ?? []) + (report?.watching ?? [])
if all.contains(where: { $0.leak.id == "bb-overfold" }) { raised += 1 }
strangers += all.filter { $0.leak.id != "bb-overfold" }.count
}
t.expect(raised >= 3,
"a player who folds every big blind was only picked up in \(raised) of "
+ "\(seeds.count) sittings")
t.lessThan(strangers, 3,
"they were handed \(strangers) other habits across \(seeds.count) "
+ "sittings; a coach that invents company for a real finding stops being "
+ "believed")
}
t.test("the report and the scorecard do not contradict each other on the same screen") {
// The app carried two bars for one question. The report names a habit when even the
// cautious end of what its chances can show is still worse than a steady player, which
// an assessment can clear off eight; the scorecard names one when there have been as
// many chances as the catalogue says it takes, which is twenty-five. So the first thing
// a new player ever did produced two cards, visible at once, giving opposite answers —
// "One habit was already clear enough to be worth naming" beside "Nothing confirmed
// yet", over a row whose own note read "Nothing is called a habit here until there have
// been 25".
//
// Neither bar moved. The assessment is evidence gathered on purpose, in the situation,
// and it carries the row — the same way a failed check does — printing its own numbers
// and saying which they are.
// Drilled with the player who plays far too many hands, because that is the habit a
// forty-hand sitting can actually prove: every unopened pot is a chance at it, so the
// interval clears the bar off about twenty-two of them. The big blind player is the
// wrong subject for this now — since its bar was re-derived to 30% over 50 chances, a
// forty-hand sitting mostly *watches* that habit rather than naming it, which is the
// honest outcome and gives this test nothing to check.
var carried = 0
var carriedByReport = 0
for seed: UInt64 in [11, 202, 3003, 40004, 500005] {
let session = playAssessment(.playsEverything, seed: seed)
guard let report = session.assessmentReport else {
t.expect(false, "no report on seed \(seed)")
continue
}
let card = LeakScorecard(of: session.history.hands, assessment: report)
for finding in report.named {
carried += 1
let listed = card.costing.map(\.id)
t.expect(listed.contains(finding.leak.id),
"the report named \(finding.leak.id) and the card, on the same hands, "
+ "left it off: \(listed)")
guard let row = card.entries.first(where: { $0.id == finding.leak.id }) else {
t.expect(false, "\(finding.leak.id) is not on the card at all")
continue
}
t.equal(row.sampleNote, nil,
"the row denies the finding it is carrying: \"\(row.sampleNote ?? "")\"")
// Where the hands in view have enough chances of their own they are the better
// evidence and they speak; the assessment only carries a row that cannot.
if !row.enoughChances {
carriedByReport += 1
t.expect(row.frequencyLine.contains("The assessment put this on your list"),
"the row does not say where its evidence came from: "
+ "\"\(row.frequencyLine)\"")
t.expect(row.frequencyLine.contains("\(finding.times) of \(finding.chances)"),
"the row does not print the assessment's own numbers: "
+ "\"\(row.frequencyLine)\"")
}
t.expect(!card.summary.contains("Nothing is costing you"),
"the summary denies the row under it: \"\(card.summary)\"")
}
// And nothing the report only watched is promoted to a finding by this.
for finding in report.watching {
t.expect(!card.costing.map(\.id).contains(finding.leak.id)
|| card.entries.first { $0.id == finding.leak.id }?.enoughChances == true,
"\(finding.leak.id) was watched, not named, and the card called it a habit")
}
}
t.greaterThan(carried, 0,
"five sittings named nothing at all, so nothing above was checked")
t.greaterThan(carriedByReport, 0,
"every finding had enough chances of its own, so the case this test "
+ "exists for — the report carrying a row the hands cannot — never came up")
}
t.test("what it names outright, it has actually earned") {
// Anything in the confirmed list has to survive the arithmetic behind it, not just the
// coach's opinion. This is the check that the strict tier really is strict.
//
// A single sitting can legitimately name nothing and watch nothing, so the loops below
// are counted rather than trusted: five sittings that all came back empty would run
// every assertion here nought times and still report a pass, which is the same disease
// as a measurement taken on one lucky seed.
var namedSeen = 0
var watchedSeen = 0
for seed: UInt64 in [11, 202, 3003, 40004, 500005] {
let report = playAssessment(.playsEverything, seed: seed).assessmentReport
for finding in (report?.named ?? []) + (report?.watching ?? []) {
t.expect(finding.chances >= AssessmentReport.minimumChances,
"\(finding.leak.id) was raised off \(finding.chances) chances")
}
for finding in report?.named ?? [] {
namedSeen += 1
t.expect(finding.times >= 3, "\(finding.leak.id) was named off \(finding.times)")
t.expect(finding.lowEnd > finding.leak.acceptableRate,
"\(finding.leak.id) was named without clearing a steady player's rate")
t.expect(finding.certain)
}
for finding in report?.watching ?? [] {
watchedSeen += 1
t.expect(!finding.certain)
t.expect(finding.line.contains("not enough to call that a habit"),
"an unproven finding has to say so: \(finding.line)")
}
}
t.greaterThan(namedSeen, 0, "five sittings named nothing, so the strict tier was never checked")
t.greaterThan(watchedSeen, 0, "five sittings watched nothing, so the cautious tier was never checked")
}
t.test("what it could not get near, it says it could not get near") {
// Two of the planted players have flaws that only exist deep in a hand — on the river,
// or facing a bet on the flop. Forty hands often never gets there. What matters is that
// the report says nothing about them rather than inventing a verdict, and that it lists
// the parts of the game it never saw instead of letting silence read as approval.
for player in [PlantedPlayer.neverValueBetsRiver, .foldsToEveryFlopBet] {
var wrongNames = 0
for seed: UInt64 in [11, 202, 3003, 40004, 500005] {
let report = playAssessment(player, seed: seed).assessmentReport
wrongNames += (report?.named ?? []).filter { $0.leak.id != player.leakID }.count
if let note = report?.thinNote {
t.expect(note.contains("not the same as saying they are fine"),
"the thin note has to refuse the wrong reading")
}
}
t.lessThan(wrongNames, 2,
"a player who \(player.name) was confidently told about \(wrongNames) "
+ "habits they were never actually measured on")
}
}
t.test("a player with nothing wrong is told nothing is wrong") {
var invented = 0
for seed: UInt64 in [11, 202, 3003, 40004, 500005] {
let report = playAssessment(.control, seed: seed).assessmentReport
invented += (report?.named.count ?? 0) + (report?.watching.count ?? 0)
}
t.lessThan(invented, 3,
"forty hands handed a sound player \(invented) findings across five "
+ "sittings — at this sample the answer should almost always be silence")
}
}
}