Files
HexTrails/tests/route-grammar.test.js
T

67 lines
4.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { canRotate, opposite, rotateBoard, trace, validateLevel } from '../src/core.js';
import { createRouteBoard, ROUTE_FAMILIES } from '../src/route-grammar.js';
const pairKey = (q, r, a, b) => `${q},${r}:${Math.min(a, b)},${Math.max(a, b)}`;
const boardKey = board => board.cells.map(cell => `${cell.q},${cell.r}:${cell.tracks.join('')}`).sort().join('|');
const featuredKey = board => board.routeDesign.featured.map(pair => pairKey(pair.q, pair.r, pair.a, pair.b)).sort().join('|');
for (const { id: family } of ROUTE_FAMILIES) {
test(`${family}: complete rotating boards contain real open hero paths at every supported size`, () => {
for (const radius of [2, 3, 4, 5]) for (const seed of [0, 1, 7, 42, 71, 95]) {
const board = createRouteBoard(seed, { family, radius });
const label = `${family}, radius ${radius}, seed ${seed}`;
assert.deepEqual(validateLevel(board), [], label);
assert.equal(board.cells.length, 1 + 3 * radius * (radius + 1), label);
assert.equal(canRotate(board), true, label);
assert.equal(boardKey(rotateBoard(board, 6)), boardKey(board), label);
assert.deepEqual(board.tiles, [], label);
assert.ok(board.routeDesign.heroStarts.length > 0, label);
const boardCells = new Map(board.cells.map(cell => [`${cell.q},${cell.r}`, cell]));
const covered = new Set();
let longestBends = 0;
for (const [index, hero] of board.routeDesign.heroStarts.entries()) {
const tile = { ...hero, id: `hero-${index}`, color: 0 };
// Exercise the production tracer, including swept geometry, not the
// grammar's own inexpensive route scan used to propose candidates.
const route = trace(board, tile, [tile]);
assert.equal(route.status, 'exit', label);
assert.equal(route.steps.length, hero.steps, label);
const bends = route.steps.filter(step => step.enter !== null && opposite(step.enter) !== step.exit).length;
assert.equal(bends, hero.bends, label);
longestBends = Math.max(longestBends, bends);
for (const step of route.steps) if (step.enter !== null) covered.add(pairKey(step.q, step.r, step.enter, step.exit));
}
assert.ok(longestBends >= 7, `${label}: actual open path should sustain a long curved run`);
assert.ok(board.routeDesign.featured.length > 0, label);
for (const pair of board.routeDesign.featured) {
const cell = boardCells.get(`${pair.q},${pair.r}`);
assert.equal(cell?.tracks[pair.a], pair.b, `${label}: emphasized rail must be an actual pair`);
assert.ok(covered.has(pairKey(pair.q, pair.r, pair.a, pair.b)), `${label}: emphasized rail must have a supplied open-path witness`);
}
}
});
test(`${family}: variant changes the authored route and remains deterministic`, () => {
const original = createRouteBoard(42, { family, radius: 3, variant: 0 });
const variant = createRouteBoard(42, { family, radius: 3, variant: 1 });
assert.deepEqual(createRouteBoard(42, { family, radius: 3, variant: 0 }), original);
assert.notEqual(featuredKey(original), featuredKey(variant), 'A variant changes real featured routing, not only background fill');
assert.notEqual(boardKey(original), boardKey(rotateBoard(original)), 'The grammar must allow rotation to change paths');
});
}
test('the six route families are geometrically different even after whole-board rotation', () => {
const boards = ROUTE_FAMILIES.map(({ id }) => createRouteBoard(42, { family: id, radius: 3 }));
const canonical = boards.map(board => Array.from({ length: 6 }, (_, turn) => boardKey(rotateBoard(board, turn))).sort()[0]);
assert.equal(new Set(canonical).size, ROUTE_FAMILIES.length);
});
test('route grammar rejects unsupported board requests', () => {
assert.throws(() => createRouteBoard(1, { family: 'unknown' }), /Unknown route family/);
for (const radius of [1, 6, 2.5]) assert.throws(() => createRouteBoard(1, { radius }), /radius/);
assert.throws(() => createRouteBoard(1, { variant: 0.5 }), /variant/);
});