Load-bearing in one mode
Both duel pages held their own copy of the same reveal-timing math. One input to the shared version looks redundant for AI duel and is anything but for PvP.
The same review’s next find, right after the notice queue, was smaller in scope but familiar in shape: both duel pages independently computed the same answer to the same question, how long to hold the recap back after a collision so the player registers what happened before a winner gets declared. The deletion test confirmed what that usually means: deleting either page’s copy today would just push the same fifteen-odd lines into the other page, not remove any real complexity.
The fix follows the same small-selector shape the word panel already settled on: one function taking the game’s current facts and returning exactly one of three answers, with both pages reduced to a single call reacting to whatever it returns.
export type GameOverReveal =
| { kind: 'hidden' }
| { kind: 'immediate' }
| { kind: 'delayed'; delayMs: number };
One of the inputs to that function looks redundant the moment you read it in isolation: whether an animation is still playing, when the function is already told the game is over. For the local AI duel it genuinely is a no-op, since that mode’s game state can never be both “over” and “still animating” at the same instant. For PvP it is the input that keeps the whole thing correct, because PvP’s phase comes over a live connection from a server that can mark a game finished while the losing player’s final turn is still animating out on the loser’s own screen. Drop that check and the recap could arrive before the collision it’s supposed to be recapping.
Five new tests cover all three outcomes plus how the wave-duration scaling that feeds the delayed case actually behaves, coverage the inline version, copy-pasted into two files with no shared test file of its own, never had.