Files
CodeNavigator/webtest/nav.test.mjs
T
Andy Dixon 9bc5ba483c Add wandering residents to the city
Little glowing green stick figures, in the spirit of Darwinia, stroll
the streets: each walks toward a random reachable point in plain sight
on the navigation grid, pauses, and about a third of the time heads
toward a nearby resident instead, so they gather in small crowds. They
are camera-facing sprites drawn procedurally in the fragment shader
with a walk cycle, flicker and halo, grown with altitude so crowds stay
visible from the helicopter, and fogged like everything else.

About one resident per 2,500 m² of city (40 to 600), spawned only on
the city's own streets. Because they keep the city animating, Settings
has a City residents switch (saved with the other preferences). The
simulation is seedable and tested to keep everyone on walkable ground.
2026-06-20 10:52:58 +01:00

83 lines
4.7 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildNavGrid, routesFrom, walkable } from '../web/city.js';
// Two buildings separated by a long wall-like building with a gap at the far end.
const a = { x: 0, y: 0, w: 10, h: 10, height: 20 };
const b = { x: 60, y: 0, w: 10, h: 10, height: 20 };
const wall = { x: 30, y: -40, w: 6, h: 100, height: 20 };
const bounds = { width: 100, height: 100 };
const onFreeGround = (grid, points) => {
for (let i = 1; i < points.length; i++) {
const [p, q] = [points[i - 1], points[i]], steps = Math.ceil(Math.hypot(q[0] - p[0], q[1] - p[1]));
for (let s = 0; s <= steps; s++) assert.ok(walkable(grid, p[0] + (q[0] - p[0]) * s / steps, p[1] + (q[1] - p[1]) * s / steps), `segment ${i} crosses a building`);
}
};
test('routes go around buildings and never through them', () => {
const grid = buildNavGrid([a, b, wall], [], bounds);
const [route] = routesFrom(grid, a, [b]);
assert.ok(route && route.length >= 3, 'needs turns to get around the wall');
onFreeGround(grid, route);
assert.ok(route.some(([, y]) => y > 60), 'detours past the end of the wall');
const toEdge = ([x, y], r) => Math.hypot(Math.max(r.x - x, 0, x - r.x - r.w), Math.max(r.y - y, 0, y - r.y - r.h));
assert.ok(toEdge(route[0], a) < 8 && toEdge(route.at(-1), b) < 8, `starts and ends beside the buildings: ${route[0]} ${route.at(-1)}`);
});
test('several targets from one search; enclosed ones end at the nearest street', () => {
const sealed = { x: 80, y: 80, w: 6, h: 6, height: 5 };
const box = [{ x: 74, y: 74, w: 18, h: 2, height: 5 }, { x: 74, y: 90, w: 18, h: 2, height: 5 }, { x: 74, y: 74, w: 2, h: 18, height: 5 }, { x: 90, y: 74, w: 2, h: 18, height: 5 }];
const grid = buildNavGrid([a, b, sealed, ...box], [], { width: 120, height: 120 }, 3, .5);
const routes = routesFrom(grid, a, [b, sealed]);
assert.ok(routes[0]);
onFreeGround(grid, routes[0]);
// The sealed building can't be entered, so its route ends on the nearest reachable street outside the ring.
onFreeGround(grid, routes[1]);
const end = routes[1].at(-1);
assert.ok(Math.hypot(end[0] - 83, end[1] - 83) < 20 && !(end[0] > 74 && end[0] < 92 && end[1] > 74 && end[1] < 92), `ends outside the ring: ${end}`);
});
test('routes prefer roads over pavements when the detour is small', () => {
const pavement = { depth: 1, x: 12, y: -2, w: 40, h: 14 };
const grid = buildNavGrid([a, b], [pavement], bounds);
const [route] = routesFrom(grid, a, [b]);
onFreeGround(grid, route);
assert.ok(route.length >= 2);
});
import { layoutCity } from '../web/city.js';
test('routing a large generated city stays fast (guards against queue flooding)', () => {
const files = []; let id = 1;
for (let d = 0; d < 40; d++) for (let s = 0; s < 6; s++) for (let f = 0; f < 7; f++) files.push({ id: id++, path: `dir${d}/sub${s}/file${f}.go`, lines: 50 + (id * 7919) % 3000, complexity: (id * 31) % 200 });
const city = layoutCity(files), entries = [...city.buildings.values()], grid = buildNavGrid(entries, city.blocks, city);
const started = performance.now(), routes = routesFrom(grid, entries[0], entries.slice(100, 140));
assert.ok(performance.now() - started < 3000, `took ${performance.now() - started} ms`);
assert.ok(routes.every(Boolean), 'every building in a connected city is reachable');
routes.forEach(route => { for (const [x, y] of route) assert.ok(walkable(grid, x, y)); });
});
import { spawnWanderers, stepWanderers, seededRandom } from '../web/city.js';
test('wanderers spawn and stay on walkable ground while they roam', () => {
const grid = buildNavGrid([a, b, wall], [], bounds), random = seededRandom(7);
const crowd = spawnWanderers(grid, 60, random);
assert.equal(crowd.length, 60);
const start = crowd.map(w => [w.x, w.y]);
for (let frame = 0; frame < 900; frame++) {
stepWanderers(crowd, grid, 1 / 30, random);
if (frame % 30 === 0) for (const w of crowd) assert.ok(walkable(grid, w.x, w.y), `walker at ${w.x},${w.y}`);
}
const moved = crowd.filter((w, i) => Math.hypot(w.x - start[i][0], w.y - start[i][1]) > 3).length;
assert.ok(moved > 40, `most walkers wandered off (${moved})`);
assert.ok(crowd.every(w => Number.isFinite(w.phase)));
assert.ok(spawnWanderers(grid, 30, random, { width: 20, height: 20 }).every(w => w.x >= 0 && w.x <= 20 && w.y >= 0 && w.y <= 20), 'spawns inside the city bounds');
});
test('a seeded crowd is reproducible', () => {
const grid = buildNavGrid([a], [], bounds);
const run = () => { const r = seededRandom(3), c = spawnWanderers(grid, 10, r); for (let i = 0; i < 100; i++) stepWanderers(c, grid, .05, r); return c.map(w => [w.x.toFixed(3), w.y.toFixed(3)]); };
assert.deepEqual(run(), run());
});