Files
CodeNavigator/webtest/city.test.mjs
T
Andy Dixon 9c7643c616 License under GPLv3
Add SPDX headers to the source, a License section to the README, the
OFL notice for the bundled fonts, and ship LICENSE in the Docker image.
2026-09-20 10:05:06 +01:00

110 lines
5.5 KiB
JavaScript

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 Andy Dixon
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { layoutCity, buildingHeight, spatialIndex, rayBox, pickRay, CITY } from '../web/city.js';
function sampleFiles() {
const files = []; let id = 1;
for (const dir of ['src/core', 'src/ui/widgets', 'tests', 'docs', 'vendor/lib', '']) {
for (let i = 0; i < 12; i++) files.push({ id: id++, path: `${dir ? dir + '/' : ''}file${i}.js`, lines: 20 + (i * 137) % 900, complexity: i * 3 });
}
return files;
}
const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
test('every file gets a building inside the city, and buildings never overlap', () => {
const files = sampleFiles(), city = layoutCity(files, 'demo');
assert.equal(city.buildings.size, files.length);
const list = [...city.buildings.values()];
for (const b of list) {
assert.ok(b.w > 0 && b.h > 0 && b.x >= 0 && b.y >= 0 && b.x + b.w <= city.width + 1e-6 && b.y + b.h <= city.height + 1e-6, JSON.stringify(b));
}
for (let i = 0; i < list.length; i++) for (let j = i + 1; j < list.length; j++) assert.ok(!overlaps(list[i], list[j]), `${i} overlaps ${j}`);
});
test('top-level districts are separated by an avenue', () => {
const city = layoutCity(sampleFiles(), 'demo');
const districts = city.blocks.filter(block => block.depth === 1);
assert.ok(districts.length >= 4);
for (let i = 0; i < districts.length; i++) for (let j = i + 1; j < districts.length; j++) {
const a = districts[i], b = districts[j];
const gap = Math.max(b.x - (a.x + a.w), a.x - (b.x + b.w), b.y - (a.y + a.h), a.y - (b.y + b.h));
assert.ok(gap >= CITY.roadWidths[0] * .99 || gap >= Math.min(a.w, a.h, b.w, b.h) * .39, `gap ${gap}`);
}
});
test('the city grows with the codebase and is deterministic', () => {
const small = layoutCity(sampleFiles().slice(0, 10)), large = layoutCity(sampleFiles());
assert.ok(large.width * large.height > small.width * small.height * 3);
assert.deepEqual([...layoutCity(sampleFiles()).buildings], [...large.buildings]);
});
test('complex files are taller', () => {
assert.ok(buildingHeight({ complexity: 200 }) > buildingHeight({ complexity: 3 }));
assert.equal(buildingHeight({}), 6);
});
test('rays hit the nearest box and miss empty space', () => {
const near = { x: 10, y: -1, w: 2, h: 2, height: 5 }, far = { x: 30, y: -1, w: 2, h: 2, height: 50 };
assert.equal(rayBox([0, 0, 1], [1, 0, 0], near), 10);
assert.equal(rayBox([0, 0, 6], [1, 0, 0], near), Infinity);
assert.equal(pickRay([0, 0, 1], [1, 0, 0], [far, near]).entry, near);
assert.equal(pickRay([0, 0, 1], [-1, 0, 0], [far, near]), null);
assert.equal(pickRay([0, 0, 1], [1, 0, 0], [far, near], 5), null);
});
test('spatial index finds boxes near a point', () => {
const a = { x: 0, y: 0, w: 5, h: 5 }, b = { x: 200, y: 200, w: 5, h: 5 };
const index = spatialIndex([a, b]);
assert.deepEqual([...index.near(2, 2, 3)], [a]);
assert.deepEqual([...index.near(100, 100, 3)], []);
});
import { collide, heading, stepCamera, blockAt, groundHit, MOVE } from '../web/city.js';
test('collision pushes a walker out of buildings but leaves streets alone', () => {
const box = { x: 0, y: 0, w: 10, h: 10, height: 20 }, index = spatialIndex([box]);
assert.deepEqual(collide(20, 20, .5, index), { x: 20, y: 20 });
const edge = collide(10.2, 5, .5, index);
assert.ok(Math.abs(edge.x - 10.5) < 1e-9 && edge.y === 5);
const inside = collide(9, 5, .5, index);
assert.ok(Math.abs(inside.x - 10.5) < 1e-9, `inside exits nearest wall: ${inside.x}`);
});
test('headings are orthogonal and yaw 0 faces north', () => {
for (const yaw of [0, .7, 2, -2.5]) {
const { forward, right } = heading(yaw);
assert.ok(Math.abs(forward[0] * right[0] + forward[1] * right[1]) < 1e-12);
}
assert.deepEqual(heading(0).forward.map(v => Math.round(v)), [-0, -1]);
});
test('walking moves at walking speed, collides, and stays on the ground', () => {
const index = spatialIndex([{ x: -5, y: -20, w: 10, h: 5, height: 30 }]);
const cam = { view: 'walk', ex: 0, ey: 0, ez: 1.7, lookYaw: 0, lookPitch: .5 };
stepCamera(cam, { forward: 1, right: 0, up: 1 }, 1, index, { width: 100, height: 100 });
assert.ok(Math.abs(cam.ey + MOVE.walk) < 1e-9 && cam.ez === MOVE.eyeHeight);
stepCamera(cam, { forward: 1, right: 0, up: 0 }, 2, index, { width: 100, height: 100 });
assert.ok(cam.ey >= -15 + MOVE.radius - 1e-9, `stopped at the wall, got ${cam.ey}`);
});
test('flying climbs with the view and clears low roofs', () => {
const index = spatialIndex([{ x: -5, y: -20, w: 10, h: 5, height: 10 }]);
const cam = { view: 'fly', ex: 0, ey: 0, ez: 50, lookYaw: 0, lookPitch: 0 };
stepCamera(cam, { forward: 1, right: 0, up: 0 }, 1, index, { width: 100, height: 100 });
assert.ok(cam.ey < -30, 'flew over the low building');
stepCamera(cam, { forward: 0, right: 0, up: -1 }, 10, index, { width: 100, height: 100 });
assert.equal(cam.ez, MOVE.eyeHeight);
});
test('addresses resolve to the deepest block and rays find the ground', () => {
const blocks = [{ depth: 0, x: 0, y: 0, w: 100, h: 100 }, { depth: 1, x: 0, y: 0, w: 50, h: 50, node: { path: 'src' } }, { depth: 2, x: 10, y: 10, w: 10, h: 10, node: { path: 'src/ui' } }];
assert.equal(blockAt(blocks, 15, 15).node.path, 'src/ui');
assert.equal(blockAt(blocks, 40, 40).node.path, 'src');
assert.equal(blockAt(blocks, 90, 90), null);
assert.deepEqual(groundHit([0, 0, 10], [0, Math.SQRT1_2, -Math.SQRT1_2]).map(v => Math.round(v)), [0, 10]);
assert.equal(groundHit([0, 0, 10], [0, 0, 1]), null);
});