games: the short stack that could win money it never matched

A review pass, and it found the one that would have cost somebody real chips.

Side pots were only ever cut in runout() — the path taken when the betting
stops because nobody is left able to bet. But a hand reaches a showdown with an
all-in player in it and the betting having finished perfectly normally: a short
stack shoves, two players who still have chips behind call, and then keep
betting past them street after street to the river. Nothing was cut. One pot,
everybody eligible, and the short stack takes the lot — every chip the deep
players put in after they were already all-in, money that could never have been
lost to them. All-in for 100 against two players who each put in 500, and the
best hand collects 1,100 instead of the 300 it was playing for.

Chip conservation never saw it. The chips balance perfectly; they just land in
the wrong seat. And every browser session went through runout(), because a
player shoving is what ends the betting. It took reading the code.

Also from the review: play() dereferenced a table it had just been handed as
null, the top-up button offered chips the wallet could not cover, and the
trainer's ETA was sixty thousand hands optimistic on the first line it printed.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
prosolis
2026-07-14 09:16:52 -07:00
parent 903c5accdb
commit b96879d25c
8 changed files with 127 additions and 34 deletions

View File

@@ -258,14 +258,14 @@ func (s State) spotKey(seat int, eq Equity) string {
// ---- choosing --------------------------------------------------------------
// Hits and Misses count how often a bot finds itself in the trained policy.
// hits and misses count how often a bot finds itself in the trained policy.
//
// They exist because the way this can break is silently. A key the policy has
// never seen is not an error — the bot shrugs and plays pot odds — so a policy
// that has stopped matching the game looks exactly like a policy that is working.
// gogobee's never matched once, for the whole life of the game, and nobody could
// have known by watching it play. Now a test reads these and fails.
var Hits, Misses atomic.Int64
var hits, misses atomic.Int64
// botActs plays one bot's turn: work out where it stands, look up what it does
// there, throw out anything illegal, and roll for it.
@@ -274,9 +274,9 @@ func (s *State) botActs(seat int, evs *[]Event, rng *rand.Rand) {
probs, ok := loadPolicy()[key]
if ok {
Hits.Add(1)
hits.Add(1)
} else {
Misses.Add(1)
misses.Add(1)
probs = potOdds(eq, s, seat)
}
probs = legal(probs, s, seat)