games: the seat you sit in, and the seat you are left in

Six-handed, the felt printed CO on three seats at once. Position walked the
table with nextIn, which steps over folded seats, while the seat count it walked
against still included them — so every muck slid the anchors round and the
labels landed somewhere new. Folding the small blind relabelled it the cutoff.

The two walks are a pair and they are easy to confuse. nextIn asks who is still
in the betting; a fold takes you out of it. Position needs the other question —
who was dealt in — because where you sit is decided when the button moves and
does not change because somebody threw their hand away. So nextDealt, which
skips only the seats that are not in the hand at all, and a note at both of them
saying which is which.

The bots never read this. They use InPosition, which really does want the last
seat still live, and which is deliberately not this function. So the policy is
untouched and the money never moved — the only thing this ever broke was the
badge on the plate, which is precisely why nothing caught it.

TestPositionsDoNotMoveWhenSeatsFold deals six-handed, asserts the table prints
each of BTN/SB/BB/UTG/MP/CO exactly once, then folds the seats out from under it
one at a time and asserts nobody's label moves.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
prosolis
2026-07-14 11:42:18 -07:00
parent 8db8845feb
commit 03524aefbc
2 changed files with 65 additions and 4 deletions

View File

@@ -731,6 +731,24 @@ func (s State) nextIn(from int) int {
return from
}
// nextDealt is the next seat holding cards this hand, folded or not. Where
// nextIn asks "who is still in the betting", this asks "who was dealt in", and
// the pair is easy to confuse: fold three seats and nextIn walks straight past
// them, so anything counting seats round the table lands somewhere different
// depending on how the hand has gone. Position needs the fixed one — where you
// sit is decided when the button moves and does not change because somebody
// mucked.
func (s State) nextDealt(from int) int {
n := len(s.Seats)
for i := 1; i <= n; i++ {
next := (from + i) % n
if s.Seats[next].State != Out {
return next
}
}
return from
}
// onlyActor is the one seat that can still act. Call it when canActCount is 1.
func (s State) onlyActor() int {
for i := range s.Seats {
@@ -838,8 +856,8 @@ func (s State) Position(seat int) string {
return "BB" // heads-up, the other seat is always the big blind
}
sb := s.nextIn(s.Button)
bb := s.nextIn(sb)
sb := s.nextDealt(s.Button)
bb := s.nextDealt(sb)
switch seat {
case sb:
return "SB"
@@ -847,7 +865,7 @@ func (s State) Position(seat int) string {
return "BB"
}
utg := s.nextIn(bb)
utg := s.nextDealt(bb)
if seat == utg {
return "UTG"
}
@@ -856,7 +874,7 @@ func (s State) Position(seat int) string {
// closest to the button is the cutoff.
dist, cur := 0, utg
for i := 0; i < n; i++ {
cur = s.nextIn(cur)
cur = s.nextDealt(cur)
dist++
if cur == seat {
break