120 lines
3.6 KiB
JavaScript
120 lines
3.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import {
|
|
anchor,
|
|
createGame,
|
|
getActiveGateIds,
|
|
move,
|
|
parseLevel,
|
|
removeLastEcho,
|
|
reset,
|
|
validateLevel,
|
|
} from '../game-engine.js';
|
|
|
|
const ONE_RELAY = [
|
|
'#########',
|
|
'#@..aA.X#',
|
|
'#.#######',
|
|
'#.#######',
|
|
'#########',
|
|
];
|
|
|
|
const DOUBLE_RELAY = [
|
|
'#############',
|
|
'#@..aA...B.X#',
|
|
'#.###########',
|
|
'#.....b######',
|
|
'#############',
|
|
];
|
|
|
|
function drive(state, commands) {
|
|
return [...commands].reduce((next, command) => move(next, command), state);
|
|
}
|
|
|
|
test('parseLevel identifies a rectangular relay, start, goal, pads, and gates', () => {
|
|
const level = parseLevel(ONE_RELAY);
|
|
|
|
assert.equal(level.width, 9);
|
|
assert.equal(level.height, 5);
|
|
assert.deepEqual(level.start, { x: 1, y: 1 });
|
|
assert.deepEqual(level.goal, { x: 7, y: 1 });
|
|
assert.deepEqual(level.pads.get('a'), { x: 4, y: 1 });
|
|
assert.deepEqual(level.gates.get('A'), { x: 5, y: 1 });
|
|
});
|
|
|
|
test('validateLevel rejects ragged maps and unpaired signal identifiers', () => {
|
|
assert.throws(
|
|
() => validateLevel(['#####', '#@X#', '#####']),
|
|
/same width/,
|
|
);
|
|
assert.throws(
|
|
() => validateLevel(['#####', '#@aX#', '#####']),
|
|
/paired/,
|
|
);
|
|
});
|
|
|
|
test('a sealed gate blocks the runner before an echo occupies its matching pad', () => {
|
|
const beforeGate = drive(createGame(ONE_RELAY), 'RRR');
|
|
const blocked = move(beforeGate, 'R');
|
|
|
|
assert.deepEqual(blocked.player, { x: 4, y: 1 });
|
|
assert.equal(blocked.lastEvent, 'sealed');
|
|
assert.deepEqual(getActiveGateIds(blocked), []);
|
|
});
|
|
|
|
test('anchoring at a signal pad leaves an echo, resets the runner, and opens its gate', () => {
|
|
const onPad = drive(createGame(ONE_RELAY), 'RRR');
|
|
const anchored = anchor(onPad);
|
|
|
|
assert.deepEqual(anchored.player, { x: 1, y: 1 });
|
|
assert.deepEqual(anchored.echoes.map((echo) => echo.position), [{ x: 4, y: 1 }]);
|
|
assert.deepEqual(getActiveGateIds(anchored), ['A']);
|
|
assert.equal(anchored.lastEvent, 'anchor');
|
|
});
|
|
|
|
test('a relay becomes complete only after its matching echo opens the route to extraction', () => {
|
|
const anchored = anchor(drive(createGame(ONE_RELAY), 'RRR'));
|
|
const escaped = drive(anchored, 'RRRRRR');
|
|
|
|
assert.equal(escaped.status, 'won');
|
|
assert.deepEqual(escaped.player, { x: 7, y: 1 });
|
|
});
|
|
|
|
test('an anchor cannot be created off a signal pad and respects its capacity', () => {
|
|
const fresh = createGame(ONE_RELAY, { maxEchoes: 1 });
|
|
const rejected = anchor(fresh);
|
|
const first = anchor(drive(fresh, 'RRR'));
|
|
const atCap = anchor(drive(first, 'LLL'));
|
|
|
|
assert.equal(rejected.lastEvent, 'anchor-required');
|
|
assert.equal(rejected.echoes.length, 0);
|
|
assert.equal(first.echoes.length, 1);
|
|
assert.equal(atCap.lastEvent, 'anchor-required');
|
|
assert.equal(atCap.echoes.length, 1);
|
|
});
|
|
|
|
test('undo and reset restore deterministic relay state', () => {
|
|
const withEcho = anchor(drive(createGame(ONE_RELAY), 'RRR'));
|
|
const undone = removeLastEcho(withEcho);
|
|
const resetState = reset(withEcho);
|
|
|
|
assert.equal(undone.echoes.length, 0);
|
|
assert.deepEqual(undone.player, { x: 1, y: 1 });
|
|
assert.equal(resetState.echoes.length, 0);
|
|
assert.equal(resetState.status, 'playing');
|
|
assert.equal(resetState.moves, 0);
|
|
});
|
|
|
|
test('two independent echoes can sustain a staged two-gate extraction route', () => {
|
|
let state = createGame(DOUBLE_RELAY, { maxEchoes: 2 });
|
|
state = anchor(drive(state, 'DDRRRRR'));
|
|
state = anchor(drive(state, 'RRR'));
|
|
|
|
assert.deepEqual(getActiveGateIds(state), ['A', 'B']);
|
|
|
|
state = drive(state, 'RRRRRRRRRR');
|
|
assert.equal(state.status, 'won');
|
|
assert.deepEqual(state.player, { x: 11, y: 1 });
|
|
});
|