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.test.js
engine-fast.test.js 6.0K
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) => ({ id, kind: 'object', noun, x, y });78function compareStep(level, action, expectedWordMotion = { player: [], automaton: [] }) {9 const publicGame = createGame(level);10 const fastGame = createGame(level);11 const publicResult = action(publicGame, false);12 const fastResult = action(fastGame, true);13 assert.equal(fastResult.accepted, publicResult.accepted);14 assert.deepEqual(fastResult.state, publicGame.getState());15 assert.deepEqual(fastResult.wordMotion, expectedWordMotion);16 assert.equal(fastGame.canUndo, false);17 assert.equal(publicGame.canUndo, publicResult.accepted);18 return fastResult;19}2021test('stepFast matches public movement and keeps a compact rule trace', () => {22 const level = {23 id: 'fast-move', width: 6, height: 3,24 objects: [25 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),26 object('MIAU', 1, 2, 'player'),27 ],28 };29 const result = compareStep(level, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0));30 assert.equal(result.accepted, true);31 assert.equal(result.ruleTrace[0].phase, 'initial');32 assert.ok(result.ruleTrace.some(entry => entry.rules.includes('MIAU IS YOU')));33});3435test('waitFast matches public automaton phase advancement', () => {36 const level = {37 id: 'fast-clock', width: 6, height: 3,38 objects: [39 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),40 word('CLOCK', 0, 1), word('IS', 1, 1), word('TICK', 2, 1),41 object('MIAU', 0, 2, 'player'), object('CLOCK', 3, 2, 'clock'),42 ],43 automata: [{ id: 'clockwork', subject: 'CLOCK', property: 'TICK', pattern: [{ action: 'wait' }, { action: 'move', dx: 1, dy: 0 }] }],44 };45 const result = compareStep(level, (game, fast) => fast ? game.waitFast() : game.wait());46 assert.equal(result.state.automata[0].phase, 1);47 assert.deepEqual(result.wordMotion, { player: [], automaton: [] });48});4950test('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});6869test('fast provenance separates player and automaton word motion', () => {70 const level = {71 id: 'fast-provenance', width: 7, height: 4,72 objects: [73 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),74 word('CLOCK', 0, 1), word('IS', 1, 1), word('TICK', 2, 1), word('AND', 3, 1), word('PUSH', 4, 1),75 object('MIAU', 0, 2, 'player'), object('CLOCK', 1, 3, 'clock'), word('ROCK', 2, 3, 'loose-word'),76 ],77 automata: [{ id: 'clockwork', subject: 'CLOCK', property: 'TICK', pattern: [{ action: 'move', dx: 1, dy: 0 }] }],78 };79 const result = compareStep(level, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0), { player: [], automaton: ['loose-word'] });80 assert.deepEqual(result.wordMotion.automaton, ['loose-word']);81});8283test('fast provenance retains automaton motion for a word already moved by the player', () => {84 const level = {85 id: 'fast-provenance-merge', width: 6, height: 4,86 objects: [87 word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0),88 word('CLOCK', 0, 1), word('IS', 1, 1), word('TICK', 2, 1),89 word('ROCK', 0, 2), word('IS', 1, 2), word('PUSH', 2, 2),90 object('MIAU', 0, 3, 'player'), word('ROCK', 1, 3, 'rock-word'), object('CLOCK', 3, 3, 'clock'),91 ],92 automata: [{ id: 'clockwork', subject: 'CLOCK', property: 'TICK', pattern: [{ action: 'move', dx: -1, dy: 0 }] }],93 };94 const result = compareStep(level, (game, fast) => fast ? game.stepFast(1, 0) : game.step(1, 0), { player: ['rock-word'], automaton: ['rock-word'] });95 assert.deepEqual(result.wordMotion, { player: ['rock-word'], automaton: ['rock-word'] });96});9798test('blocked fast input does not create history or mutate settled state', () => {99 const level = {100 id: 'fast-blocked', width: 3, height: 2,101 objects: [word('MIAU', 0, 0), word('IS', 1, 0), word('YOU', 2, 0), object('MIAU', 0, 1, 'player')],102 };103 const game = createGame(level);104 const before = game.getState();105 const result = game.stepFast(-1, 0);106 assert.equal(result.accepted, false);107 assert.equal(result.blocked.reason, 'bounds');108 assert.deepEqual(result.state, before);109 assert.equal(game.canUndo, false);110});111112test('fast parser-limit rejection rolls back without creating history', () => {113 const objects = [114 word('BABA', 0, 0, 'subject'), word('IS', 1, 1, 'loose-is'),115 word('MIAU', 0, 3, 'miau-subject'), word('IS', 1, 3, 'miau-is'), word('YOU', 2, 3, 'miau-you'),116 object('MIAU', 1, 2, 'player'),117 ];118 for (let index = 0; index < 15; index += 1) {119 const x = 2 + index * 2;120 objects.push(word('WIN', x, 0, `win-${index}`), word('PUSH', x, 0, `push-${index}`));121 if (index < 14) objects.push(word('AND', x + 1, 0, `and-${index}`));122 }123 const game = createGame({ id: 'fast-limit', width: 31, height: 4, objects });124 const before = game.getState();125 const result = game.stepFast(0, -1);126 assert.equal(result.accepted, false);127 assert.equal(result.blocked.reason, 'rule-complexity');128 assert.deepEqual(result.state, before);129 assert.equal(game.canUndo, false);130});