89 lines
5 KiB
JavaScript
89 lines
5 KiB
JavaScript
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.');
|