Summary
<game-board> tracks win state (hasWon, movesSinceWin) and lastAppliedJson as sticky per-instance fields with no game-identity key. When a single element instance is reused across different games, that state leaks between games and mislabels the endgame badge.
Where it bites
The splash is one <game-board id="splash-board"> whose splashState signal is overwritten with different games as it cycles/seeks through all_states (serve.nu /sse/splash/:tabId + seed at the page route). The same element instance receives snapshots from many distinct games over its lifetime.
- Once any splashed game crosses 2048,
hasWon latches true for every subsequent game on that element. A later game that never won still renders the win path — "game over" + "you win!" instead of "you lost" (#applyBadge: showWin = over ? this.hasWon : ..., showLost = over && !this.hasWon).
movesSinceWin ticks on every distinct snapshot, so across game boundaries the "hide the win badge after 3 post-win moves" rule is meaningless.
State only resets when a fresh element is created (page reload).
/play and the /my/games / /by/<id> cards are unaffected — there it's one element ↔ one game. /watch would be exposed if it ever swaps games on a single element.
Root cause
game-board.js dedupes purely on JSON equality (lastAppliedJson) and has no notion of "this is now a different game," so #apply can't know to reset hasWon / movesSinceWin.
Suggested fix
Carry a game identity in the wire state (e.g. gameId) and reset hasWon, movesSinceWin, and lastAppliedJson in #apply when it changes — or expose a game-id attribute the splash sets per seek and reset in attributeChangedCallback.
Secondary (minor)
movesSinceWin++ keys off "distinct JSON," not "a move." Any non-move re-render (e.g. a playedMs-only change) would tick it. Latent today because playedMs only changes on a move, but the counter is coupled to serialization rather than to game events.
File: examples/2048/static/game-board.js (#tickWinCounter, #applyBadge, attributeChangedCallback).
Summary
<game-board>tracks win state (hasWon,movesSinceWin) andlastAppliedJsonas sticky per-instance fields with no game-identity key. When a single element instance is reused across different games, that state leaks between games and mislabels the endgame badge.Where it bites
The splash is one
<game-board id="splash-board">whosesplashStatesignal is overwritten with different games as it cycles/seeks throughall_states(serve.nu/sse/splash/:tabId+ seed at the page route). The same element instance receives snapshots from many distinct games over its lifetime.hasWonlatchestruefor every subsequent game on that element. A later game that never won still renders the win path — "game over" + "you win!" instead of "you lost" (#applyBadge:showWin = over ? this.hasWon : ...,showLost = over && !this.hasWon).movesSinceWinticks on every distinct snapshot, so across game boundaries the "hide the win badge after 3 post-win moves" rule is meaningless.State only resets when a fresh element is created (page reload).
/playand the/my/games//by/<id>cards are unaffected — there it's one element ↔ one game./watchwould be exposed if it ever swaps games on a single element.Root cause
game-board.jsdedupes purely on JSON equality (lastAppliedJson) and has no notion of "this is now a different game," so#applycan't know to resethasWon/movesSinceWin.Suggested fix
Carry a game identity in the wire state (e.g.
gameId) and resethasWon,movesSinceWin, andlastAppliedJsonin#applywhen it changes — or expose agame-idattribute the splash sets per seek and reset inattributeChangedCallback.Secondary (minor)
movesSinceWin++keys off "distinct JSON," not "a move." Any non-move re-render (e.g. aplayedMs-only change) would tick it. Latent today becauseplayedMsonly changes on a move, but the counter is coupled to serialization rather than to game events.File:
examples/2048/static/game-board.js(#tickWinCounter,#applyBadge,attributeChangedCallback).