clone

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

main
miau-is-true / tests / engine-fast-parity.test.js
engine-fast-parity.test.js 5.3K
1import test from 'node:test';2import assert from 'node:assert/strict';3import { createGame } from '../src/engine.js';45const word = (text, x, y, id = `${text}-${x}-${y}`) => ({ id, kind: 'word', text, x, y });6const object = (noun, x, y, id = `${noun}-${x}-${y}`) => ({ id, kind: 'object', noun, x, y });7const level = (objects, extra = {}) => ({ width: 8, height: 5, objects, ...extra });89function settledState(state) {10 return {11 width: state.width,12 height: state.height,13 objects: state.objects.slice().sort((a, b) => a.id.localeCompare(b.id)),14 rules: state.rules,15 sentences: state.sentences,16 automata: state.automata,17 won: state.won,18 };19}2021function assertParity(board, action) {22 const publicGame = createGame(board);23 const fastGame = createGame(board);24 const publicResult = action(publicGame, false);25 const fastResult = action(fastGame, true);26 assert.equal(fastResult.accepted, publicResult.accepted);27 assert.deepEqual(settledState(fastResult.state), settledState(publicGame.getState()));28 assert.deepEqual(settledState(fastGame.getState()), settledState(publicGame.getState()));29 assert.equal(fastGame.canUndo, false, 'fast transitions must not create undo history');30 return { publicGame, fastGame, publicResult, fastResult };31}3233test('fast step matches W6 stacked rule parsing and movement', () => {34 const board = level([35 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),36 word('ROCK', 0, 1), word('AND', 1, 1), word('IS', 1, 1), word('FLAG', 2, 1),37 object('MIAU', 0, 2, 'player'),38 ]);39 const { fastResult } = assertParity(board, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0));40 assert.ok(fastResult.ruleTrace.some(entry => entry.rules.includes('ROCK IS FLAG')));41});4243test('fast blocked transition matches PUSH and STOP semantics', () => {44 const board = level([45 word('BABA', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),46 word('ROCK', 0, 1), word('IS', 1, 1), word('PUSH', 2, 1),47 word('WALL', 0, 2), word('IS', 1, 2), word('STOP', 2, 2),48 object('BABA', 1, 3, 'player'), object('ROCK', 2, 3, 'rock'), object('ROCK', 3, 3, 'rock-2'), object('WALL', 4, 3, 'wall'),49 ]);50 const { fastResult } = assertParity(board, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0));51 assert.deepEqual(fastResult.blocked, { id: 'rock-2', dir: [1, 0], blockerId: 'wall', reason: 'stop' });52});5354test('fast step matches singleton DEFEAT and WIN contact ordering', () => {55 const board = level([56 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),57 word('ROCK', 0, 1), word('IS', 1, 1), word('DEFEAT', 2, 1),58 word('FLAG', 0, 2), word('IS', 1, 2), word('WIN', 2, 2),59 object('MIAU', 0, 3, 'player'), object('ROCK', 1, 3, 'defeat'), object('FLAG', 1, 3, 'goal'),60 ]);61 const { fastResult } = assertParity(board, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0));62 assert.equal(fastResult.won, false);63 assert.equal(fastResult.state.objects.some(entry => entry.id === 'player'), false);64});6566test('fast step matches noun-chain transformation', () => {67 const board = level([68 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),69 word('ROCK', 0, 1), word('IS', 1, 1), word('FLAG', 2, 1),70 word('FLAG', 0, 2), word('IS', 1, 2), word('WIN', 2, 2),71 object('MIAU', 0, 3, 'player'), object('ROCK', 3, 3, 'rock'),72 ]);73 assertParity(board, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0));74});7576test('fast step matches SINK rule-word removal and reparse', () => {77 const board = level([78 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),79 word('WATER', 3, 1), word('IS', 4, 1), word('SINK', 5, 1),80 word('ROCK', 1, 3, 'rock-word'), word('IS', 2, 3, 'rock-is'), word('STOP', 3, 3, 'stop-word'),81 object('MIAU', 0, 3, 'player'), object('WATER', 4, 3, 'water'),82 ]);83 const { fastResult } = assertParity(board, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0));84 assert.equal(fastResult.state.rules.some(rule => rule.text === 'ROCK IS STOP'), false);85 assert.ok(fastResult.ruleTrace.some(entry => entry.rules.includes('ROCK IS STOP')));86});8788test('fast wait matches Clockwork automaton phase advancement', () => {89 const board = level([90 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),91 word('COG', 0, 1), word('IS', 1, 1), word('TICK', 2, 1),92 object('MIAU', 0, 3, 'player'), object('COG', 3, 3, 'cog'),93 ], { automata: [{ id: 'clock', subject: 'COG', property: 'TICK', phase: 0, pattern: [{ action: 'wait' }, { action: 'move', dx: 1, dy: 0 }] }] });94 const { fastResult } = assertParity(board, (game, fast) => fast ? game.waitFast() : game.wait());95 assert.equal(fastResult.state.automata[0].phase, 1);96});9798test('fast wait reports automaton word movement separately from player work', () => {99 const board = level([100 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),101 word('COG', 0, 1), word('IS', 1, 1), word('TICK', 2, 1),102 word('COG', 0, 2), word('IS', 1, 2), word('PUSH', 2, 2),103 object('MIAU', 0, 4, 'player'), object('COG', 2, 3, 'cog'), word('WIN', 3, 3, 'moving-word'),104 ], { automata: [{ id: 'clock', subject: 'COG', property: 'TICK', phase: 0, pattern: [{ action: 'move', dx: 1, dy: 0 }] }] });105 const { fastResult } = assertParity(board, (game, fast) => fast ? game.waitFast() : game.wait());106 assert.deepEqual(fastResult.wordMotion.player, []);107 assert.deepEqual(fastResult.wordMotion.automaton, ['moving-word']);108});