Back to Pursuit PIT
SOURCE / PINNED RELEASE

Made of little things.

Pursuit PIT

Release
565f4981bdc3…
Author-recorded commit
197afdbed44b…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7

Archive hash verified: 3b0485589942fd5c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tools/road-check.ts
// Checks the generated road and its signs over a long drive, for several seeds:
// smooth bends and slopes within limits, consistent heading/elevation, and sane town names.
// Run with: bun tools/road-check.ts
import { MAX_GRADE, MIN_RADIUS, MOUNTAIN_MIN_RADIUS, curvature, elevation, grade, heading, setRoadSeed } from '../src/game/road.ts';
import { signsIn } from '../src/game/signs.ts';
import { TOWN_NAMES_FOR_TESTS, town } from '../src/game/towns.ts';
import { closedLanes, closureOf, leftWall, townSpan, zoneAt, type Area } from '../src/game/zones.ts';

const LENGTH = 200_000;
const failures: string[] = [];
const fail = (msg: string) => {
  if (failures.length < 20) failures.push(msg);
};

for (const seed of [1, 2, 3, 12345, 0xdeadbeef]) {
  setRoadSeed(seed);
  let maxK = 0, maxKOut = 0, maxG = 0, maxDK = 0, maxDG = 0, maxElev = 0;
  let prevK = curvature(-500), prevG = grade(-500);
  for (let s = -500; s < LENGTH; s += 1) {
    const k = curvature(s), g = grade(s);
    maxK = Math.max(maxK, Math.abs(k));
    // Away from the mountains (and their blends) bends stay gentle.
    if (zoneAt(s - 400).area !== 'mountains' && zoneAt(s).area !== 'mountains' && zoneAt(s + 400).area !== 'mountains') maxKOut = Math.max(maxKOut, Math.abs(k));
    maxG = Math.max(maxG, Math.abs(g));
    maxDK = Math.max(maxDK, Math.abs(k - prevK));
    maxDG = Math.max(maxDG, Math.abs(g - prevG));
    maxElev = Math.max(maxElev, Math.abs(elevation(s)));
    prevK = k;
    prevG = g;
  }
  if (maxK > 1 / MOUNTAIN_MIN_RADIUS + 1e-9) fail(`seed ${seed}: curvature ${maxK} too tight`);
  if (maxKOut > 1 / MIN_RADIUS + 1e-9) fail(`seed ${seed}: mountain-tight bend outside the mountains (${maxKOut})`);
  if (maxG > MAX_GRADE + 1e-9) fail(`seed ${seed}: grade ${maxG} too steep`);
  // Per metre, curvature and grade change smoothly (no step at segment joins).
  if (maxDK > 2 / MOUNTAIN_MIN_RADIUS / 100) fail(`seed ${seed}: curvature jumps by ${maxDK}`);
  if (maxDG > (2 * MAX_GRADE) / 100) fail(`seed ${seed}: grade jumps by ${maxDG}`);

  // Heading and elevation are integrals: compare with a numeric sum, and query out of order.
  let h = heading(50_000), y = elevation(50_000);
  for (let s = 50_000; s < 60_000; s += 0.5) {
    h += curvature(s + 0.25) * 0.5;
    y += grade(s + 0.25) * 0.5;
  }
  setRoadSeed(seed); // fresh caches, reached from the far end first
  if (Math.abs(heading(60_000) - h) > 1e-4) fail(`seed ${seed}: heading integral off by ${heading(60_000) - h}`);
  if (Math.abs(elevation(60_000) - y) > 1e-3) fail(`seed ${seed}: elevation integral off by ${elevation(60_000) - y}`);
  if (Math.abs(heading(-3_000) - heading(-3_000)) > 0) fail('heading not repeatable');

  // Towns: no repeats among neighbours, nothing on the block list.
  const names: string[] = [];
  for (let j = 0; j < 60; j++) {
    const name = town(j, seed).name;
    if (names.slice(-10).includes(name)) fail(`seed ${seed}: town ${name} repeats within 10`);
    const squashed = name.toLowerCase().replace(/[^a-z]/g, '');
    if (TOWN_NAMES_FOR_TESTS.BLOCK.some((b) => squashed.includes(b))) fail(`seed ${seed}: blocked name ${name}`);
    names.push(name);
  }

  // Signs: a stable schedule, and windows agree with the whole.
  const all = signsIn(0, 20_000);
  const parts = [...signsIn(0, 7_000), ...signsIn(7_000, 20_000)];
  if (all.length !== parts.length) fail(`seed ${seed}: sign windows disagree (${all.length} vs ${parts.length})`);
  if (new Set(all.map((s) => s.key)).size !== all.length) fail(`seed ${seed}: duplicate sign keys`);
  const kinds = new Map<string, number>();
  for (const sign of all) kinds.set(sign.kind, (kinds.get(sign.kind) ?? 0) + 1);
  console.log(`seed ${seed}: R min ${Math.round(1 / maxK)} m, grade max ${(maxG * 100).toFixed(1)}%, |elev| max ${Math.round(maxElev)} m,`,
    'signs in 20 km:', Object.fromEntries(kinds), '| towns:', names.slice(0, 6).join(', '));
}

// Areas: towns in order, two different areas between each pair, closures inside road works.
for (const seed of [1, 2, 3, 12345]) {
  setRoadSeed(seed);
  const lengths = new Map<Area, number[]>();
  let prev = zoneAt(0), wallPrev = leftWall(0), maxWallStep = 0, towns = 0;
  for (let s = 10; s < 100_000; s += 10) {
    const z = zoneAt(s);
    if (s < z.start || s >= z.end) fail(`seed ${seed}: zone at ${s} is ${z.start}..${z.end}`);
    if (z.start !== prev.start) {
      if (prev.end !== z.start) fail(`seed ${seed}: gap or overlap at ${s}`);
      const list = lengths.get(prev.area) ?? [];
      list.push(prev.end - prev.start);
      lengths.set(prev.area, list);
      const bothCountry = prev.area !== 'town' && prev.area !== 'city' && z.area !== 'town' && z.area !== 'city';
      if (bothCountry && prev.area === z.area && prev.start > 0) fail(`seed ${seed}: ${z.area} twice in a row at ${s}`);
      if (z.area === 'town' || z.area === 'city') {
        towns++;
        // Exactly two areas since the last town.
        const before = zoneAt(z.start - 1), earlier = zoneAt(before.start - 1);
        if (z.town! > 0 && (earlier.area === 'town' || earlier.area === 'city' ? 0 : 1) + 1 !== 2) fail(`seed ${seed}: town ${z.town} not after two areas`);
        if (z.town! > 0 && zoneAt(earlier.start - 1).area !== 'town' && zoneAt(earlier.start - 1).area !== 'city') fail(`seed ${seed}: more than two areas before town ${z.town}`);
      }
      prev = z;
    }
    const c = closedLanes(s);
    if (c && !closureOf(z)) fail(`seed ${seed}: lanes closed outside road works or mountains at ${s}`);
    const wall = leftWall(s);
    maxWallStep = Math.max(maxWallStep, Math.abs(wall - wallPrev));
    wallPrev = wall;
  }
  if (maxWallStep > 1) fail(`seed ${seed}: barrier line jumps ${maxWallStep.toFixed(2)} m in 10 m`);
  const towns0 = townSpan(1).start - townSpan(0).end;
  const summary = [...lengths].map(([a, l]) => `${a} ${l.length}x ~${Math.round(l.reduce((x, y) => x + y) / l.length)} m`).join(', ');
  console.log(`seed ${seed}: ${towns} towns in 100 km (gap after the first ${Math.round(towns0)} m); ${summary}`);
}

if (failures.length) {
  console.log('FAIL\n' + failures.join('\n'));
  process.exit(1);
}
console.log('OK');