- commit
- 163c20756a671a08c904e58b9c8352864772442a
- parent
- d11fa16b104f41b5bbc77b4d109d1033de1b07e7
- author
- Dami <dami.etoile@gmail.com>
| M | openspec/ | 2 | +- |
| M | tests/ | 107 | ++++++++++++++++++++++++++++++ |
@@ -2,7 +2,7 @@
22 33 - [ ] 1.1 Record current solver route, expansion, time, and memory baselines for core, Word Lab, and representative Crossword, Crate Works, Tidewater, and Clockwork proofs.44 - [x] 1.2 Inventory every field in settled engine snapshots and identify which fields determine future transitions, including automata phases and current transformation nouns.5-- [ ] 1.3 Add a regression that restoring a settled state and taking one step matches direct play for noun chains, automata, removal, and rule edits.5+- [x] 1.3 Add a regression that restoring a settled state and taking one step matches direct play for noun chains, automata, removal, and rule edits.66 77 ## 2. Canonical compact codec88 @@ -85,3 +85,110 @@ test('codec rejects unregistered top-level and authored object fields', () => {
8585 assert.throws(() => codec.encode({ ...state(), objects: state().objects.map(object => ({ ...object, usesRemaining: 1 })) }), /unregistered object field/);8686 assert.throws(() => codec.encode({ ...state(), won: true }), /winning snapshots are terminal/);8787 });88+89+test('codec round trip preserves the next turn after SINK removes an entity', () => {90+ const sinkLevel = {91+ id: 'codec-sink', width: 4, height: 3,92+ objects: [93+ { id: 'miau-word', kind: 'word', text: 'MIAU', x: 0, y: 0 },94+ { id: 'miau-is', kind: 'word', text: 'IS', x: 1, y: 0 },95+ { id: 'miau-you', kind: 'word', text: 'YOU', x: 2, y: 0 },96+ { id: 'water-word', kind: 'word', text: 'WATER', x: 0, y: 1 },97+ { id: 'water-is', kind: 'word', text: 'IS', x: 1, y: 1 },98+ { id: 'water-sink', kind: 'word', text: 'SINK', x: 2, y: 1 },99+ { id: 'left-miau', kind: 'object', noun: 'MIAU', x: 0, y: 2 },100+ { id: 'right-miau', kind: 'object', noun: 'MIAU', x: 2, y: 2 },101+ { id: 'water', kind: 'object', noun: 'WATER', x: 1, y: 2 },102+ ],103+ };104+ const direct = createGame(sinkLevel);105+ const first = direct.step(1, 0);106+ assert.equal(first.accepted, true);107+ assert.equal(direct.getState().objects.some(object => object.id === 'left-miau'), false);108+ const codec = createStateCodec(sinkLevel);109+ const restored = createGame(sinkLevel);110+ restored.restore(codec.decode(codec.encode(direct.getState())));111+ const directNext = direct.step(1, 0);112+ const restoredNext = restored.step(1, 0);113+ assert.deepEqual(restoredNext, directNext);114+ assert.deepEqual(restored.getState(), { ...direct.getState(), moves: 0 });115+});116+117+function assertNextTurnParity(levelDefinition, snapshot, action) {118+ const codec = createStateCodec(levelDefinition);119+ // The frontier codec intentionally resets the presentation-only move120+ // counter. Normalize the direct branch to the same settled solver baseline121+ // while comparing gameplay state and the next event.122+ const solverBaseline = { ...snapshot, moves: 0 };123+ const direct = createGame(levelDefinition);124+ direct.restore(solverBaseline);125+ const roundTripped = createGame(levelDefinition);126+ roundTripped.restore(codec.decode(codec.encode(solverBaseline)));127+128+ const directResult = action(direct);129+ const roundTrippedResult = action(roundTripped);130+ assert.deepEqual(roundTrippedResult, directResult);131+ assert.deepEqual(roundTripped.getState(), direct.getState());132+}133+134+test('compact round trip preserves the next noun-chain turn', () => {135+ const chainLevel = {136+ id: 'codec-chain', width: 6, height: 3,137+ objects: [138+ { id: 'baba-subject', kind: 'word', text: 'BABA', x: 0, y: 0 },139+ { id: 'baba-is', kind: 'word', text: 'IS', x: 1, y: 0 },140+ { id: 'baba-rock', kind: 'word', text: 'ROCK', x: 2, y: 0 },141+ { id: 'rock-subject', kind: 'word', text: 'ROCK', x: 0, y: 1 },142+ { id: 'rock-is', kind: 'word', text: 'IS', x: 1, y: 1 },143+ { id: 'rock-flag', kind: 'word', text: 'FLAG', x: 2, y: 1 },144+ { id: 'miau-subject', kind: 'word', text: 'MIAU', x: 0, y: 2 },145+ { id: 'miau-is', kind: 'word', text: 'IS', x: 1, y: 2 },146+ { id: 'miau-you', kind: 'word', text: 'YOU', x: 2, y: 2 },147+ { id: 'baba', kind: 'object', noun: 'BABA', x: 4, y: 2 },148+ { id: 'player', kind: 'object', noun: 'MIAU', x: 0, y: 2 },149+ ],150+ };151+ const snapshot = createGame(chainLevel).getState();152+ assert.equal(snapshot.objects.find(object => object.id === 'baba').noun, 'ROCK');153+ assertNextTurnParity(chainLevel, snapshot, game => game.step(1, 0));154+});155+156+test('compact round trip preserves the next automaton wait phase', () => {157+ const automatonLevel = {158+ id: 'codec-clock', width: 6, height: 4,159+ objects: [160+ { id: 'miau-subject', kind: 'word', text: 'MIAU', x: 0, y: 0 },161+ { id: 'miau-is', kind: 'word', text: 'IS', x: 1, y: 0 },162+ { id: 'miau-you', kind: 'word', text: 'YOU', x: 2, y: 0 },163+ { id: 'clock-subject', kind: 'word', text: 'CLOCK', x: 0, y: 1 },164+ { id: 'clock-is', kind: 'word', text: 'IS', x: 1, y: 1 },165+ { id: 'clock-tick', kind: 'word', text: 'TICK', x: 2, y: 1 },166+ { id: 'player', kind: 'object', noun: 'MIAU', x: 0, y: 3 },167+ { id: 'clock', kind: 'object', noun: 'CLOCK', x: 2, y: 3 },168+ ],169+ automata: [{ id: 'clock', subject: 'CLOCK', property: 'TICK', phase: 1, pattern: [{ action: 'wait' }, { action: 'wait' }] }],170+ };171+ const snapshot = createGame(automatonLevel).getState();172+ assert.equal(snapshot.automata[0].phase, 1);173+ assertNextTurnParity(automatonLevel, snapshot, game => game.wait());174+});175+176+test('compact round trip preserves the next turn after a word-rule edit', () => {177+ const editLevel = {178+ id: 'codec-word-edit', width: 6, height: 4,179+ objects: [180+ { id: 'miau-subject', kind: 'word', text: 'MIAU', x: 0, y: 3 },181+ { id: 'miau-is', kind: 'word', text: 'IS', x: 1, y: 3 },182+ { id: 'miau-you', kind: 'word', text: 'YOU', x: 2, y: 3 },183+ { id: 'rock-subject', kind: 'word', text: 'ROCK', x: 0, y: 1 },184+ { id: 'rock-is', kind: 'word', text: 'IS', x: 1, y: 1 },185+ { id: 'rock-stop', kind: 'word', text: 'STOP', x: 2, y: 1 },186+ { id: 'player', kind: 'object', noun: 'MIAU', x: 1, y: 2 },187+ ],188+ };189+ const game = createGame(editLevel);190+ const edited = game.step(0, -1);191+ assert.equal(edited.accepted, true);192+ assert.equal(game.has('ROCK', 'STOP'), false);193+ assertNextTurnParity(editLevel, game.getState(), next => next.step(1, 0));194+});