map/tests/r11.7-terrain-routed-transport-density.mjs

89 lines
5 KiB
JavaScript
Raw Permalink Normal View History

2026-08-11 21:51:07 +09:00
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { Worker } from 'node:worker_threads';
function runWorker(seed) {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL('./helpers-generation-worker-node-wrapper.mjs', import.meta.url), { type: 'module' });
const timer = setTimeout(() => { worker.terminate(); reject(new Error(`worker timeout seed ${seed}`)); }, 120000);
worker.on('error', (error) => { clearTimeout(timer); reject(error); });
worker.on('message', async (message) => {
if (message?.type !== 'result' || message.id !== seed) return;
clearTimeout(timer); worker.removeAllListeners(); await worker.terminate();
if (!message.ok) reject(new Error(message.error || 'generation failed')); else resolve(message.map);
});
worker.postMessage({ id: seed, seed, options: { terrainType: 'auto' } });
});
}
function pathLength(paths) {
let sum = 0;
for (const path of paths || []) for (let i = 1; i < (path?.length || 0); i++) sum += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]);
return sum;
}
function maxGap(paths) {
let max = 0;
for (const path of paths || []) for (let i = 1; i < (path?.length || 0); i++) max = Math.max(max, Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]));
return max;
}
function pointPathDistance(point, paths) {
let best = Infinity;
for (const path of paths || []) for (const q of path || []) best = Math.min(best, Math.hypot(q[0] - point.x, q[1] - point.y));
return best;
}
function endpointContinuityAudit(map) {
const bad = [];
const exp = map.expressways || [];
const ordinary = [...(map.nationalRoads || []), ...(map.minorRoads || []), ...(map.externalRoads || [])];
const cities = (map.modernCities || []).filter((c) => (c.population || 0) >= 50000);
for (let i = 0; i < exp.length; i++) {
const path = exp[i]; if (!path?.length) continue;
const others = exp.filter((_, j) => j !== i);
for (const tuple of [path[0], path[path.length - 1]]) {
const pt = { x: tuple[0], y: tuple[1] };
const edge = pt.x < 2 || pt.y < 2 || pt.x > map.width - 3 || pt.y > map.height - 3;
const interchange = (map.interchanges || []).some((ic) => Math.hypot(ic.x - pt.x, ic.y - pt.y) <= 6.5);
const city = cities.some((c) => Math.hypot(c.x - pt.x, c.y - pt.y) <= 28);
const connected = edge || pointPathDistance(pt, others) <= 3.5 || pointPathDistance(pt, ordinary) <= 4.5 || interchange || city;
if (!connected) bad.push({ path: i, x: pt.x, y: pt.y });
}
}
return bad;
}
const source = fs.readFileSync(new URL('../src/mapPostAdminTransport.js', import.meta.url), 'utf8');
assert(!source.includes('function directPath('), 'post-admin transport has no directPath straight-line fallback');
assert(!source.includes('function directLandConnector('), 'transport has no directLandConnector straight-line fallback');
assert(source.includes('if (gap > maxJoinGap) return []'), 'failed chain legs reject the whole trunk instead of drawing a straight chord');
assert(source.includes('removeDiscontinuousTransportPaths(2.25)'), 'final production invariant removes sparse-jump transport paths');
const map = await runWorker(1);
const trunk = {
national: map.nationalRoads || [],
expressway: map.expressways || [],
rail: [...(map.railways || []), ...(map.branchRailways || [])],
};
for (const [name, paths] of Object.entries(trunk)) {
assert(maxGap(paths) <= Math.SQRT2 + 1e-6, `${name}: every emitted segment is raster-contiguous; no renderer straight chord remains`);
}
const nationalLength = pathLength(trunk.national);
const railLength = pathLength(trunk.rail);
assert(nationalLength > 0 && railLength / nationalLength >= 0.80 && railLength / nationalLength <= 1.01,
`rail density remains high (${(railLength / nationalLength).toFixed(3)}) and approximately national-road scale`);
const expressLength = pathLength(trunk.expressway);
assert((map.interchanges || []).length >= Math.max(2, Math.floor(expressLength / 18)),
`IC density is sufficient for ${expressLength.toFixed(1)} expressway cells`);
assert.equal(endpointContinuityAudit(map).length, 0, 'expressways have no unjustified interior dead-end endpoints');
const ordinary = [...(map.minorRoads || []), ...(map.nationalRoads || []), ...(map.externalRoads || [])];
const villages = map.villages || [];
const ruralServed = villages.filter((v) => pointPathDistance(v, ordinary) <= 4).length;
assert(!villages.length || ruralServed / villages.length >= 0.80, `rural village road coverage is ${(ruralServed / Math.max(1, villages.length)).toFixed(3)}`);
assert((map.minorRoads || []).length >= 60, 'published countryside retains a substantial local-road network');
const post = map.transportDebug?.postAdminTransportFinalization || {};
assert((post.finalInterchangeRebuild?.added || 0) >= (post.finalInterchangeRebuild?.target || 0), 'final IC rebuild meets its production target');
assert(post.finalDiscontinuousTransportCleanup, 'final discontinuity cleanup ran');
console.log('All r11.7 terrain-routed transport density regression checks passed.');