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) { return (paths || []).reduce((sum, path) => { let length = 0; for (let i = 1; i < (path?.length || 0); i++) length += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]); return sum + length; }, 0); } function prefCounts(map) { const groups = new Map(); for (let i = 0; i < map.adminId.length; i++) { if (map.sea[i]) continue; const a = Number(map.adminId[i]), p = Number(map.prefectureRegionId[i]); if (a < 0 || p < 0) continue; if (!groups.has(p)) groups.set(p, new Set()); groups.get(p).add(a); } return [...groups.values()].map((s) => s.size); } 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 parallelRun(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; 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; } function extremeRun(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; run = angle >= 82 ? run + 1 : 0; best = Math.max(best, run); } return best; } function edgeHits(paths, width, height) { return (paths || []).filter((path) => (path || []).some(([x, y]) => x <= 0 || y <= 0 || x >= width - 1 || y >= height - 1)).length; } function pointPathDistance(point, path) { let best = Infinity; for (const tuple of path || []) best = Math.min(best, Math.hypot(tuple[0] - point.x, tuple[1] - point.y)); return best; } function clearInteriorMinorOrphans(map) { const minor = map.minorRoads || []; const trunks = [...(map.nationalRoads || []), ...(map.externalRoads || []), ...(map.expressways || []), ...(map.railways || []), ...(map.branchRailways || [])]; const civic = [...(map.modernCities || []), ...(map.markets || []), ...(map.villages || []), ...(map.ports || []), ...(map.adminCenters || []).filter(Boolean)]; const connected = (tuple, self) => { const pt = { x: tuple[0], y: tuple[1] }; if (trunks.some((path) => pointPathDistance(pt, path) <= 2.5)) return true; return minor.some((path, index) => index !== self && pointPathDistance(pt, path) <= 2.2); }; const out = []; for (let index = 0; index < minor.length; index++) { const path = minor[index]; if (!path?.length) continue; let length = 0; for (let k = 1; k < path.length; k++) length += Math.hypot(path[k][0] - path[k - 1][0], path[k][1] - path[k - 1][1]); if (length >= 24) continue; if (path.some(([x, y]) => x <= 0.5 || y <= 0.5 || x >= map.width - 1.5 || y >= map.height - 1.5)) continue; if (civic.some((point) => pointPathDistance(point, path) <= 2.6)) continue; const a = path[0], b = path[path.length - 1]; if (!connected(a, index) && !connected(b, index)) out.push({ index, length }); } return out; } for (const seed of [1, 2, 5]) { console.log('r11.5 audit seed', seed); const map = await runWorker(seed); console.log('r11.5 generated seed', seed); const post = map.transportDebug?.postAdminTransportFinalization || {}; const visible = post.visibleCropMajorCityService; const hidden = post.postDedupeMajorCityService; const visibleFinalizer = post.initialVisibleCropFinalizer; assert.equal(map.initialGenerationOverscan?.version, 'literal-hidden-raster-center-crop-v1', `seed ${seed}: literal production overscan`); assert(visibleFinalizer?.productionOnly && visibleFinalizer?.simplifiedOutputForbidden, `seed ${seed}: visible finalizer uses full production output only`); assert(post.visibleCropOrphanMinorRoadCleanup?.enabled === true, `seed ${seed}: crop-induced minor-road orphan cleanup ran on published raster`); assert.equal(clearInteriorMinorOrphans(map).length, 0, `seed ${seed}: no short crop-created interior minor road is disconnected at both ends without serving a settlement`); assert.equal(visible?.missing?.length || 0, 0, `seed ${seed}: every publishable major city gets all required trunk modes`); assert.equal(visible?.edgeTruncated?.length || 0, 0, `seed ${seed}: crop-edge is no longer an excuse for missing major-city trunk service`); for (const ex of visible?.geographicExceptions || []) { assert(ex.visibleLandComponentArea < 96 && ex.national && ex.rail && !ex.expressway, `seed ${seed}: only tiny-islet motorway omission is allowed`); } assert.equal(hidden?.missing?.length || 0, 0, `seed ${seed}: hidden production audit has no unresolved major-city service failure`); const counts = prefCounts(map); assert(counts.length >= 2 && Math.min(...counts) >= 10, `seed ${seed}: every published prefecture has at least ten municipalities`); assert(map.regionalDebug?.visibleCropPrefectureRepair?.minimumVisibleMunicipalities >= 10, `seed ${seed}: post-crop prefecture repair is active`); const nationalLen = pathLength(map.nationalRoads); const railLen = pathLength([...(map.railways || []), ...(map.branchRailways || [])]); const ratio = nationalLen > 0 ? railLen / nationalLen : 0; assert(ratio >= 0.78 && ratio <= 1.00, `seed ${seed}: railway density (${ratio.toFixed(3)}) is high but remains below national-road density`); assert(parallelRun(map.nationalRoads || [], 3) <= 5, `seed ${seed}: no long ~1km-class national-road parallel corridor`); assert(parallelRun(map.expressways || [], 4) <= 5, `seed ${seed}: no long ~1km-class expressway parallel corridor`); assert(Math.max(0, ...(map.expressways || []).map(extremeRun)) <= 1, `seed ${seed}: expressway avoids repeated extreme bends`); const trunkEdgeHits = edgeHits([...(map.nationalRoads || []), ...(map.expressways || []), ...(map.railways || []), ...(map.branchRailways || [])], map.width, map.height); assert(trunkEdgeHits >= 2, `seed ${seed}: hidden OD context produces real trunk continuations across published boundary`); } const cropSource = fs.readFileSync(new URL('../src/initialGenerationCrop.js', import.meta.url), 'utf8'); const postSource = fs.readFileSync(new URL('../src/mapPostAdminTransport.js', import.meta.url), 'utf8'); const rendererSource = fs.readFileSync(new URL('../src/renderer.js', import.meta.url), 'utf8'); assert(cropSource.includes('rebalanceVisiblePrefectureMunicipalityFloor(out, 10)'), 'visible prefecture floor is applied after exact crop'); assert(cropSource.includes('pruneVisibleCropOrphanMinorRoads(out)'), 'minor-road orphan cleanup is re-run after exact crop'); assert(postSource.includes('ensureVisibleCropMajorCityInternalService') && postSource.includes('densifyNationalToRailRatio'), 'visible-core transport quality finalizers exist'); assert(rendererSource.includes('land fill and coastline share the exact same binary sea mask') && rendererSource.includes('color = centerWater ? waterColor : landColor'), 'coastline and land/water fill use the same binary sea mask'); console.log('All r11.5 visible-quality finalizer regression checks passed.');