map/tests/r11.4-transport-demography-overscan.mjs

106 lines
6.7 KiB
JavaScript
Raw Normal View History

2026-08-11 21:51:07 +09:00
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { generateMap } from '../src/mapPipeline.js';
function prefectureMunicipalityCounts(map) {
const groups = new Map();
for (let i = 0; i < map.adminId.length; i++) {
if (map.sea[i] || map.adminId[i] < 0 || map.prefectureRegionId[i] < 0) continue;
let set = groups.get(map.prefectureRegionId[i]);
if (!set) groups.set(map.prefectureRegionId[i], set = new Set());
set.add(map.adminId[i]);
}
return [...groups.values()].map((set) => set.size);
}
function pathLength(paths) { return (paths || []).reduce((sum, path) => sum + (path?.length || 0), 0); }
function maxSeaRun(path, sea, width) {
let run = 0, best = 0;
for (const [x, y] of path || []) {
const isSea = x < 0 || y < 0 || x >= width || y >= sea.length / width || sea[y * width + x];
if (isSea) { run++; best = Math.max(best, run); } else run = 0;
}
return best;
}
function maxExtremeTurnRun(path) {
let run = 0, best = 0;
for (let k = 2; k < (path?.length || 0) - 2; k += 2) {
const a = path[k - 2], b = path[k], c = path[k + 2];
const ux = b[0] - a[0], uy = b[1] - a[1], vx = c[0] - b[0], vy = c[1] - b[1];
const ud = Math.hypot(ux, uy), vd = Math.hypot(vx, vy);
if (!ud || !vd) { run = 0; continue; }
const dot = Math.max(-1, Math.min(1, (ux * vx + uy * vy) / (ud * vd)));
const angle = Math.acos(dot) * 180 / Math.PI;
if (angle >= 82) { run++; best = Math.max(best, run); } else run = 0;
}
return best;
}
function pathTangent(path, k) {
const a = path[Math.max(0, k - 2)], b = path[Math.min(path.length - 1, k + 2)];
const dx = b[0] - a[0], dy = b[1] - a[1], d = Math.hypot(dx, dy) || 1;
return [dx / d, dy / d];
}
function longestDistinctParallelRun(paths, radius) {
let worst = 0;
for (let a = 0; a < (paths?.length || 0); a++) for (let b = a + 1; b < paths.length; b++) {
let run = 0;
for (let k = 0; k < paths[a].length; k += 2) {
const p = paths[a][k], t = pathTangent(paths[a], k);
let parallel = false;
for (let q = 0; q < paths[b].length; q += 2) {
const z = paths[b][q], dx = p[0] - z[0], dy = p[1] - z[1], d2 = dx * dx + dy * dy;
// Exact shared alignment is intentional multiplexing, not parallel duplication.
if (d2 < 0.75 || d2 > radius * radius) continue;
const u = pathTangent(paths[b], q);
if (Math.abs(t[0] * u[0] + t[1] * u[1]) >= 0.90) { parallel = true; break; }
}
run = parallel ? run + 1 : 0;
worst = Math.max(worst, run);
}
}
return worst;
}
for (const seed of [114514, 999]) {
const map = generateMap(seed, { onProgress() {}, initialGenerationOverscan: false });
const populations = (map.adminCenters || []).map((p) => Number(p.municipalityPopulation || 0));
assert(populations.length >= 30, `seed ${seed}: enough municipalities`);
assert(Math.min(...populations) >= 1000, `seed ${seed}: municipality population floor is at least ~1000`);
assert(populations.filter((p) => p > 10000).length / populations.length <= 0.30, `seed ${seed}: >10k municipalities are not dominant`);
const names = (map.adminCenters || []).map((p) => String(p.name || ''));
assert(names.filter((n) => n.endsWith('市')).length / names.length <= 0.35, `seed ${seed}: 市 does not dominate municipality types`);
const prefCounts = prefectureMunicipalityCounts(map);
assert(prefCounts.length >= 2 && Math.min(...prefCounts) >= 10, `seed ${seed}: no tiny five-municipality prefecture`);
const finalTransport = map.transportDebug?.postAdminTransportFinalization || {};
assert.equal(finalTransport.postDedupeMajorCityService?.missing?.length || 0, 0, `seed ${seed}: all same-land major cities have national/rail/expressway service`);
assert.equal(map.transportDebug?.layers?.syntheticUrbanStreetMeshDisabled, true, `seed ${seed}: synthetic urban grid is disabled`);
assert.equal(map.transportDebug?.layers?.urbanStreetMeshes?.algorithm, 'existing-local-access', `seed ${seed}: urban local roads reuse the existing local-access algorithm`);
const nationalLen = pathLength(map.nationalRoads);
const railLen = pathLength([...(map.railways || []), ...(map.branchRailways || [])]);
assert(nationalLen > 0 && railLen / nationalLen >= 0.65 && railLen / nationalLen <= 1.02, `seed ${seed}: rail density is high but approximately below national-road density`);
for (const path of map.expressways || []) {
assert(maxSeaRun(path, map.sea, map.width) <= 3, `seed ${seed}: expressway does not ignore a large strait`);
assert(maxExtremeTurnRun(path) <= 1, `seed ${seed}: expressway avoids repeated extreme turns`);
}
const branch = finalTransport.finalExpresswayBranchSimplification;
assert(branch && branch.branchNodesAfter <= branch.branchNodesBefore, `seed ${seed}: expressway branch simplifier never increases branching`);
assert(finalTransport.postServiceNationalSharedAlignment && finalTransport.postServiceExpresswaySharedAlignment, `seed ${seed}: final same-direction corridor sharing pass is active`);
assert(longestDistinctParallelRun(map.expressways || [], 4) <= 5, `seed ${seed}: expressways do not remain in long close parallel corridors`);
assert(longestDistinctParallelRun(map.nationalRoads || [], 3) <= 5, `seed ${seed}: national roads do not remain in long close parallel corridors`);
}
const featureSource = fs.readFileSync(new URL('../src/mapFeatures.js', import.meta.url), 'utf8');
const rendererSource = fs.readFileSync(new URL('../src/renderer.js', import.meta.url), 'utf8');
assert(/allowMillionPlusMap\s*=.*<\s*0\.50/.test(featureSource), 'million-plus city map-level probability gate is 50%');
assert(rendererSource.includes('municipalFallbackLabels') && rendererSource.includes('!p.suppressMunicipalLabel') && rendererSource.includes('!p.seatOutsideVisibleCrop'), 'renderer suppresses municipal-seat labels whose true seat is outside the published crop instead of pinning them to a visible representative cell');
const generationWorkerSource = fs.readFileSync(new URL('../src/generationWorker.js', import.meta.url), 'utf8');
const cropSource = fs.readFileSync(new URL('../src/initialGenerationCrop.js', import.meta.url), 'utf8');
assert(generationWorkerSource.includes('__JAPAN_MAP_GENERATION_DIMENSIONS__') && generationWorkerSource.includes('cropInitialGenerationMap'), 'production initial generation uses a genuinely larger raster then crops the center');
assert(cropSource.includes('literal-hidden-raster-center-crop-v1'), 'published initial map records literal hidden-raster overscan provenance');
assert(cropSource.includes('suppressMunicipalLabel: true') && cropSource.includes('seatOutsideVisibleCrop: true'), 'crop metadata marks off-screen municipal seats as non-label anchors');
console.log('All r11.4 transport/demography/overscan regression checks passed.');