clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Guard delayed Win handoffs by game and level identity

Dami committed 7b2d498
commit
7b2d4982b8ffd6e0085fe205b611808ef30b8586
parent
6f178f8a519291dccce356ac51bb6ba77125d93d
author
Dami <dami.etoile@gmail.com>
3 files changed, +64 -3
Asrc/app/level-identity.js9++++++
Msrc/main.js10++++--
Atests/level-identity.test.js48++++++++++++++++++++++++++++++
A src/app/level-identity.js +9 -0
M src/main.js +7 -3
@@ -18,6 +18,7 @@ import { createTurnActions } from './app/turns.js';
1818 import { createDevView } from './app/dev-view.js';1919 import { createIntentRouter } from './app/intent-router.js';2020 import { createPresentationClock, parseSlowmo } from './presentation-clock.js';21+import { winHandoffIsCurrent } from './app/level-identity.js';2122 import './style.css';2223 import './overrides.css';2324 import './font.css';
@@ -238,9 +239,9 @@ function scheduleInitialWin() {
238239 refreshIndex(level.id);239240 }240241 const promote = () => {241- if (game !== expectedGame || catalog.getActiveLevel()?.id !== expectedLevelId || !game?.getState().won) return;242+ if (!winHandoffIsCurrent({ currentGame: game, expectedGame, expectedLevelId, activeLevel: catalog.getActiveLevel() })) return;242243 if (stage.scene === 'proof') {243- stage.show('win', { steps: game.getState().moves, nodeId: expectedLevelId, instant: true });244+ stage.show('win', { steps: expectedGame.getState().moves, nodeId: expectedLevelId, instant: true });244245 return;245246 }246247 // Retry after the INDEX exit on the presentation clock, preserving slowmo.
@@ -266,6 +267,8 @@ const turns = createTurnActions({
266267 announceTurn,267268 onWin: now => {268269 const level = activeLevel();270+ const expectedGame = game;271+ const expectedLevelId = level?.id;269272 if (level?.packId && level?.localId) {270273 progress.markSolved(level.packId, level.localId);271274 refreshIndex(level.id);
@@ -274,7 +277,8 @@ const turns = createTurnActions({
274277 presentationClock.clearTimeout(winTimer);275278 const beatStart = juice.frame(now).win?.beatStart ?? now;276279 winTimer = presentationClock.setTimeout(() => {277- if (game?.getState().won && catalog.getActiveLevel()?.id === level?.id) stage.show('win', { steps: game.getState().moves, nodeId: level?.id });280+ if (!winHandoffIsCurrent({ currentGame: game, expectedGame, expectedLevelId, activeLevel: catalog.getActiveLevel() })) return;281+ stage.show('win', { steps: expectedGame.getState().moves, nodeId: expectedLevelId });278282 }, Math.max(0, beatStart - now));279283 },280284 onRestart: () => {
A tests/level-identity.test.js +48 -0
@@ -0,0 +1,48 @@
1+import test from 'node:test';2+import assert from 'node:assert/strict';3+import { winHandoffIsCurrent } from '../src/app/level-identity.js';4+5+function game(won = true, moves = 0) {6+ return { getState: () => ({ won, moves }) };7+}8+9+test('delayed win remains valid for the game and stable level that created it', () => {10+ const expectedGame = game(true, 4);11+ assert.equal(winHandoffIsCurrent({12+ currentGame: expectedGame,13+ expectedGame,14+ expectedLevelId: 'pack:proof',15+ activeLevel: { id: 'pack:proof' },16+ }), true);17+});18+19+test('same stable id after pack replacement cannot promote the old game', () => {20+ const expectedGame = game(true, 4);21+ const replacementGame = game(true, 0);22+ assert.equal(winHandoffIsCurrent({23+ currentGame: replacementGame,24+ expectedGame,25+ expectedLevelId: 'pack:proof',26+ activeLevel: { id: 'pack:proof' },27+ }), false);28+});29+30+test('removing the active pack and falling back cannot promote its delayed win', () => {31+ const expectedGame = game(true, 4);32+ assert.equal(winHandoffIsCurrent({33+ currentGame: expectedGame,34+ expectedGame,35+ expectedLevelId: 'pack:proof',36+ activeLevel: { id: 'core:intro' },37+ }), false);38+});39+40+test('a game that has been undone out of a win cannot promote', () => {41+ const expectedGame = game(false, 5);42+ assert.equal(winHandoffIsCurrent({43+ currentGame: expectedGame,44+ expectedGame,45+ expectedLevelId: 'pack:proof',46+ activeLevel: { id: 'pack:proof' },47+ }), false);48+});