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:
@@ -357,7 +357,7 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
|
||||
return s, evs, nil
|
||||
}
|
||||
|
||||
// Step plays a move for whoever is to act — bot or player — and advances only as
|
||||
// step plays a move for whoever is to act — bot or player — and advances only as
|
||||
// far as the next decision, whoever's it is. Nobody's turn is taken for them.
|
||||
//
|
||||
// This is the seam the trainer plays through, and it exists so that the trainer
|
||||
@@ -365,7 +365,7 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
|
||||
// same side pots, the same money. The alternative is a second, simplified model
|
||||
// of poker written for the trainer alone — which is what gogobee had, and it is
|
||||
// why its policy encoded a game nobody was dealing.
|
||||
func Step(s State, m Move) (State, []Event, error) {
|
||||
func step(s State, m Move) (State, []Event, error) {
|
||||
if s.Phase != PhaseBetting {
|
||||
return s, nil, ErrNoHand
|
||||
}
|
||||
@@ -381,9 +381,9 @@ func Step(s State, m Move) (State, []Event, error) {
|
||||
return s, evs, nil
|
||||
}
|
||||
|
||||
// Open deals one heads-up hand at the given stacks, stopping at the first
|
||||
// open deals one heads-up hand at the given stacks, stopping at the first
|
||||
// decision. For the trainer: a table with no history and no button rotation.
|
||||
func Open(t Tier, stack0, stack1 int64, seed1, seed2 uint64) (State, error) {
|
||||
func open(t Tier, stack0, stack1 int64, seed1, seed2 uint64) (State, error) {
|
||||
if stack0 <= 0 || stack1 <= 0 {
|
||||
return State{}, ErrBadBuyIn
|
||||
}
|
||||
@@ -405,9 +405,9 @@ func Open(t Tier, stack0, stack1 int64, seed1, seed2 uint64) (State, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Clone deep-copies the table. CFR walks a tree of what-ifs, and a shallow copy
|
||||
// clone deep-copies the table. CFR walks a tree of what-ifs, and a shallow copy
|
||||
// would have every branch writing into the same deck.
|
||||
func (s State) Clone() State {
|
||||
func (s State) clone() State {
|
||||
out := s
|
||||
out.Seats = append([]Seat(nil), s.Seats...)
|
||||
out.Deck = append(cards.Deck(nil), s.Deck...)
|
||||
@@ -741,6 +741,17 @@ func (s State) onlyActor() int {
|
||||
return s.ToAct
|
||||
}
|
||||
|
||||
// anyAllIn reports whether anybody is in the hand with no chips left, which is
|
||||
// the only thing that makes side pots necessary.
|
||||
func (s State) anyAllIn() bool {
|
||||
for i := range s.Seats {
|
||||
if s.Seats[i].State == AllIn {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// canBet reports whether there is anybody left to bet *into*. With one player
|
||||
// still holding chips and the rest all-in, a raise is chips nobody can call, so
|
||||
// no raise is offered — to the player or to a bot.
|
||||
|
||||
Reference in New Issue
Block a user