- commit
- 4c34901d3a450781fe30b47c0c27c9645e7c2e44
- parent
- 8040dfb62a65011da09040865a6ba5630c1324d1
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 13 | ++++++++++--- |
| M | tests/ | 19 | +++++++++++++++++++ |
@@ -85,6 +85,14 @@ export function createGame(level) {
8585 return { ...action };8686 }),8787 }));88+ // Restore runs once per solver transition. Keep the authored identity and89+ // automaton lookup tables immutable so each restore only validates the90+ // supplied snapshot rather than rebuilding level metadata.91+ const authoredById = new Map(original.objects.map((object, index) => [92+ object.id || `o${index}`,93+ { ...object, kind: object.kind || (object.text != null ? 'word' : 'object') },94+ ]));95+ const automataById = new Map(automataDefs.map(system => [system.id, system]));8896 const dynamicProperties = new Set(automataDefs.map(system => system.property));8997 const semanticProperties = new Set([...CORE_PROPERTIES, ...dynamicProperties]);9098 let state;@@ -322,13 +330,12 @@ export function createGame(level) {
322330 if (snapshot.width !== original.width || snapshot.height !== original.height) throw new RangeError('snapshot board shape does not match this level');323331 if (snapshot.levelId !== undefined && snapshot.levelId !== original.id) throw new RangeError('snapshot level does not match this level');324332 if (!Array.isArray(snapshot.objects)) throw new TypeError('snapshot.objects must be an array');325- const authored = new Map(original.objects.map((object, index) => [object.id || `o${index}`, { ...object, kind: object.kind || (object.text != null ? 'word' : 'object') }]));326333 const ids = new Set();327334 const objects = snapshot.objects.map((object, index) => {328335 if (!object || typeof object !== 'object' || typeof object.id !== 'string') throw new TypeError(`snapshot.objects[${index}] needs an id`);329336 if (ids.has(object.id)) throw new Error(`snapshot has duplicate object id: ${object.id}`);330337 ids.add(object.id);331- const source = authored.get(object.id);338+ const source = authoredById.get(object.id);332339 if (!source) throw new Error(`snapshot object id is not authored by this level: ${object.id}`);333340 const kind = object.kind || (object.text != null ? 'word' : 'object');334341 if (kind !== source.kind) throw new TypeError(`snapshot object kind changed for ${object.id}`);@@ -344,7 +351,7 @@ export function createGame(level) {
344351 if (snapshot.automata !== undefined) {345352 if (!Array.isArray(snapshot.automata)) throw new TypeError('snapshot.automata must be an array');346353 for (const runtime of snapshot.automata) {347- const system = automataDefs.find(candidate => candidate.id === runtime?.id);354+ const system = automataById.get(runtime?.id);348355 if (!system || phaseById.has(runtime.id) || !Number.isInteger(runtime.phase) || runtime.phase < 0 || runtime.phase >= system.pattern.length) throw new TypeError(`snapshot has an invalid automaton phase: ${runtime?.id || '?'}`);349356 phaseById.set(runtime.id, runtime.phase);350357 }@@ -47,6 +47,25 @@ test('waitFast matches public automaton phase advancement', () => {
4747 assert.deepEqual(result.wordMotion, { player: [], automaton: [] });4848 });4949 50+test('repeated restore keeps fast automata state and validation parity', () => {51+ const level = {52+ id: 'restore-cache', width: 6, height: 3,53+ objects: [54+ word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),55+ word('CLOCK', 0, 1), word('IS', 1, 1), word('TICK', 2, 1),56+ object('MIAU', 1, 2, 'player'), object('CLOCK', 3, 2, 'clock'),57+ ],58+ automata: [{ id: 'clockwork', subject: 'CLOCK', property: 'TICK', pattern: [{ action: 'wait' }, { action: 'move', dx: 1, dy: 0 }] }],59+ };60+ const game = createGame(level);61+ const baseline = game.getState();62+ for (let index = 0; index < 100; index += 1) assert.deepEqual(game.restore(baseline), baseline);63+ const restoredFast = game.waitFast();64+ const freshFast = createGame(level).waitFast();65+ assert.deepEqual(restoredFast, freshFast);66+ assert.throws(() => game.restore({ ...baseline, automata: [{ id: 'missing', phase: 0 }] }), /invalid automaton phase: missing/);67+});68+5069 test('fast provenance separates player and automaton word motion', () => {5170 const level = {5271 id: 'fast-provenance', width: 7, height: 4,