games: the table that couldn't end, and the lock that let go too early
A code review of the uno table found the stuck guard had never once fired. It counted how many bots had passed in a row and wanted more of them than there are seats — but the bot loop hands the turn back the moment it comes round to you, so the count could never get there, and your own empty-handed pass was never in it. A dead table just passed the turn round forever. That is not an ugly ending, it's a game you cannot finish, and a game you cannot finish is chips you cannot cash out. So it asks the real question now: is there anything to draw, and is anyone holding a card that goes. And the table let go of itself too early. busy came off when the request landed, not when the script it came back with had finished playing — so for the seconds a bot lap takes, you could click a card at a board the server had already moved past. It comes off at the end now, like the other tables. Also: left: 0 was being dropped on its way out the door, which is the one number that matters (the seat that just went out), the deck counter didn't come back after a reshuffle, and hoisting fly() into flyNode() had quietly flattened the chip arc on every other table in the room. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
@@ -365,6 +365,57 @@ func TestPassOnlyAfterADraw(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// dead is a table nobody can move at: the deck is spent, the discard is one card
|
||||
// deep so there is nothing to reshuffle out of, and not a seat holds a card that
|
||||
// goes on the pile. Every seat can only pass, forever.
|
||||
func dead(hands [][]Card) State {
|
||||
s := rig(hands, Card{Red, Nine}, Red)
|
||||
s.Deck = nil
|
||||
return s
|
||||
}
|
||||
|
||||
// The game has to end here. It used to not: the stuck guard counted how many
|
||||
// bots had passed in a row and asked for more of them than there are seats, so
|
||||
// it never fired once, and a dead table just handed the turn round and round.
|
||||
// That is a game the player cannot finish — and a game they cannot finish is
|
||||
// chips they cannot cash out, because the cage won't let them leave a live hand.
|
||||
func TestDeadTableEnds(t *testing.T) {
|
||||
s := dead([][]Card{{{Blue, Three}}, {{Green, Five}}})
|
||||
|
||||
next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
|
||||
if err != nil {
|
||||
t.Fatalf("draw: %v", err)
|
||||
}
|
||||
if next.Phase != PhaseDone {
|
||||
t.Fatalf("nobody can move and there is nothing to draw: the game is over, not %q", next.Phase)
|
||||
}
|
||||
if next.Outcome != OutcomeStuck {
|
||||
t.Errorf("a tie on the shortest hand is nobody going out: %q", next.Outcome)
|
||||
}
|
||||
if next.Payout != 0 {
|
||||
t.Errorf("a stuck game pays nothing, not %d", next.Payout)
|
||||
}
|
||||
if !hasKind(evs, EvSettle) {
|
||||
t.Errorf("the table has to be told it's over: %+v", evs)
|
||||
}
|
||||
}
|
||||
|
||||
// And the shortest hand takes it, which is the one way a stuck table still pays.
|
||||
func TestDeadTablePaysTheShortestHand(t *testing.T) {
|
||||
s := dead([][]Card{{{Blue, Three}}, {{Green, Five}, {Green, Six}}})
|
||||
|
||||
next, _, err := ApplyMove(s, Move{Kind: MoveDraw})
|
||||
if err != nil {
|
||||
t.Fatalf("draw: %v", err)
|
||||
}
|
||||
if next.Outcome != OutcomeWon {
|
||||
t.Fatalf("one card against two is a win: %q", next.Outcome)
|
||||
}
|
||||
if next.Payout != s.Pays() {
|
||||
t.Errorf("payout %d, quoted %d — the felt has to honour what it advertised", next.Payout, s.Pays())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReshuffleRebuildsTheDeck(t *testing.T) {
|
||||
s := rig([][]Card{{{Blue, Three}}, {{Green, Five}}}, Card{Red, Nine}, Red)
|
||||
// An empty deck, and a discard with something under the top card to become one.
|
||||
|
||||
Reference in New Issue
Block a user