games: the clock beats the walk button, and the rack isn't betting

The trivia ladder handled a walk before it looked at the clock, so the
timeout only ever bit if the browser volunteered it. Sit on a question,
look it up, answer if you find it and walk if you don't, and you never
lose a ladder. The clock is now the first thing that happens to a move.

The house's chip rack was wired up as bet buttons on blackjack and
hangman: it's four spans with data-chip on them and nothing said the
handler only wanted the real ones. Clicking the house's money raised
your bet.

Hangman had two definitions of "a letter you'd guess" — unicode in the
engine, ASCII in the renderer — and a phrase with an accent in it would
have had no tile to fill and no key to fill it with. One definition now.

Plus: trivia's countdown no longer freezes at zero when the server turns
down a timeout report it was early for, questions whose wrong answer
decodes into the right one are dropped at the door, and hangman bets on
PeteFX's spot like every other table instead of its own copy of it.
This commit is contained in:
prosolis
2026-07-14 06:28:38 -07:00
parent 2d653bf439
commit 3e9b93af55
8 changed files with 120 additions and 87 deletions

View File

@@ -314,3 +314,39 @@ func TestGarbageMovesAreRefused(t *testing.T) {
t.Fatal("a refused move should leave the game alone")
}
}
// The clock has to beat the walk button, or it is not a deadline.
//
// If a dead clock could still be walked away from, the ladder would carry no
// risk at all: sit on every question for as long as you like, answer the ones
// you can look up, and walk off the ones you can't. The timeout has to be the
// first thing that happens to a move.
func TestWalkingOffADeadClockIsATimeout(t *testing.T) {
s := newGame(t, 500, "hard")
s, _ = answerRight(t, s, time.Second) // one rung banked, so a walk is otherwise legal
late := s.AskedAt.Add(s.Tier.Clock() + time.Second)
out, evs, err := ApplyMove(s, Move{Walk: true}, late)
if err != nil {
t.Fatalf("walking after the clock died should resolve, not error: %v", err)
}
if out.Outcome != OutcomeTimeout {
t.Fatalf("a walk after the clock ran out is a timeout, got %q", out.Outcome)
}
if out.Payout != 0 {
t.Fatalf("a timeout pays nothing, got %d", out.Payout)
}
if len(evs) == 0 || evs[0].Kind != "timeout" {
t.Fatalf("expected the timeout event first, got %+v", evs)
}
// And the same walk, one tick inside the limit, still banks.
intime := s.AskedAt.Add(s.Tier.Clock() - time.Millisecond)
banked, _, err := ApplyMove(s, Move{Walk: true}, intime)
if err != nil {
t.Fatalf("walk with the clock still running: %v", err)
}
if banked.Outcome != OutcomeWalked || banked.Payout <= 0 {
t.Fatalf("a walk inside the clock banks, got %q paying %d", banked.Outcome, banked.Payout)
}
}