feat(core): add deterministic echo relay engine

This commit is contained in:
Hermes Agent
2026-07-27 17:58:28 +08:00
commit 8e5631f74a
4 changed files with 344 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
.DS_Store
+213
View File
@@ -0,0 +1,213 @@
const WALL = '#';
const START = '@';
const GOAL = 'X';
const PAD = /^[a-z]$/;
const GATE = /^[A-Z]$/;
function copyPoint(point) {
return { x: point.x, y: point.y };
}
function pointKey(point) {
return `${point.x},${point.y}`;
}
function samePoint(left, right) {
return left.x === right.x && left.y === right.y;
}
function isPad(tile) {
return PAD.test(tile);
}
function isGate(tile) {
return tile !== GOAL && GATE.test(tile);
}
function isParsedLevel(value) {
return Boolean(value && Array.isArray(value.grid) && value.start && value.goal);
}
export function validateLevel(lines) {
if (!Array.isArray(lines) || lines.length < 3 || lines.some((row) => typeof row !== 'string')) {
throw new Error('A relay level needs at least three string rows.');
}
const width = lines[0].length;
if (width < 3 || lines.some((row) => row.length !== width)) {
throw new Error('Every relay row must use the same width.');
}
let starts = 0;
let goals = 0;
const pads = new Set();
const gates = new Set();
for (const row of lines) {
for (const tile of row) {
if (tile === START) starts += 1;
if (tile === GOAL) goals += 1;
if (isPad(tile)) pads.add(tile.toUpperCase());
if (isGate(tile)) gates.add(tile);
}
}
if (starts !== 1 || goals !== 1) {
throw new Error('Each relay needs exactly one start and one extraction point.');
}
if (pads.size !== gates.size || [...pads].some((id) => !gates.has(id))) {
throw new Error('Signal pads and gates must be paired one-to-one.');
}
}
export function parseLevel(lines) {
validateLevel(lines);
const pads = new Map();
const gates = new Map();
let start;
let goal;
lines.forEach((row, y) => {
[...row].forEach((tile, x) => {
const position = { x, y };
if (tile === START) start = position;
if (tile === GOAL) goal = position;
if (isPad(tile)) pads.set(tile, position);
if (isGate(tile)) gates.set(tile, position);
});
});
return {
grid: lines.map((row) => [...row]),
width: lines[0].length,
height: lines.length,
start,
goal,
pads,
gates,
};
}
function resolveLevel(source) {
return isParsedLevel(source) ? source : parseLevel(source);
}
function padAt(level, position) {
for (const [id, pad] of level.pads) {
if (samePoint(pad, position)) return id;
}
return null;
}
function activeGateSet(state) {
const occupiedByEcho = state.echoes.map((echo) => echo.position);
const active = new Set();
for (const [padId, position] of state.level.pads) {
if (occupiedByEcho.some((entity) => samePoint(entity, position))) {
active.add(padId.toUpperCase());
}
}
return active;
}
export function getActiveGateIds(state) {
return [...activeGateSet(state)].sort();
}
function tileAt(level, position) {
if (position.x < 0 || position.y < 0 || position.x >= level.width || position.y >= level.height) {
return null;
}
return level.grid[position.y][position.x];
}
function eventState(state, lastEvent) {
return { ...state, player: copyPoint(state.player), echoes: state.echoes.map((echo) => ({ ...echo, position: copyPoint(echo.position) })), lastEvent };
}
export function createGame(source, { maxEchoes = 3 } = {}) {
const level = resolveLevel(source);
if (!Number.isInteger(maxEchoes) || maxEchoes < 1) {
throw new Error('Relay capacity must be a positive integer.');
}
return {
level,
player: copyPoint(level.start),
echoes: [],
maxEchoes,
moves: 0,
status: 'playing',
lastEvent: 'ready',
};
}
const VECTORS = {
U: { x: 0, y: -1 },
D: { x: 0, y: 1 },
L: { x: -1, y: 0 },
R: { x: 1, y: 0 },
};
export function move(state, command) {
if (state.status !== 'playing') return eventState(state, 'complete');
const vector = VECTORS[command];
if (!vector) return eventState(state, 'invalid');
const target = { x: state.player.x + vector.x, y: state.player.y + vector.y };
const tile = tileAt(state.level, target);
if (tile === null) return eventState(state, 'boundary');
if (tile === WALL) return eventState(state, 'wall');
if (isGate(tile) && !activeGateSet(state).has(tile)) return eventState(state, 'sealed');
const next = {
...state,
player: target,
echoes: state.echoes.map((echo) => ({ ...echo, position: copyPoint(echo.position) })),
moves: state.moves + 1,
lastEvent: tile === GOAL ? 'extract' : 'move',
status: tile === GOAL ? 'won' : 'playing',
};
return next;
}
export function anchor(state) {
if (state.status !== 'playing') return eventState(state, 'complete');
const padId = padAt(state.level, state.player);
if (!padId || state.echoes.length >= state.maxEchoes) return eventState(state, 'anchor-required');
return {
...state,
player: copyPoint(state.level.start),
echoes: [
...state.echoes.map((echo) => ({ ...echo, position: copyPoint(echo.position) })),
{ id: `E${state.echoes.length + 1}`, padId: padId.toUpperCase(), position: copyPoint(state.player) },
],
moves: state.moves + 1,
lastEvent: 'anchor',
};
}
export function removeLastEcho(state) {
if (state.echoes.length === 0) return eventState(state, 'nothing-to-undo');
return {
...state,
player: copyPoint(state.level.start),
echoes: state.echoes.slice(0, -1).map((echo) => ({ ...echo, position: copyPoint(echo.position) })),
status: 'playing',
lastEvent: 'undo',
};
}
export function reset(state) {
return createGame(state.level, { maxEchoes: state.maxEchoes });
}
export function positionKey(position) {
return pointKey(position);
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "vanta-relay",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"test": "node --test tests/*.test.mjs",
"check": "node --check game-engine.js"
}
}
+119
View File
@@ -0,0 +1,119 @@
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 });
});