import { generateMap, MAP_W, MAP_H, indexOf } from "./mapGenerator.js"; import { CUSTOM_NAME_LIST, NAME_KANJI_POOLS, NAME_PARTS, NAME_PROBABILITIES, NAME_TEMPLATES, NAME_TEMPLATE_WEIGHTS, generateEntityName, generateTemplateName, validateGeneratedName, } from "./names.js"; const IS_BROWSER = typeof document !== "undefined"; const result = IS_BROWSER ? document.getElementById("result") : { className: "", textContent: "" }; const logLines = []; let failed = 0; async function readLocalText(path) { if (IS_BROWSER) return fetch(path).then((response) => response.text()); const { readFile } = await import("node:fs/promises"); return readFile(new URL(path, import.meta.url), "utf8"); } const [namesSource, mapGeneratorSource, mapOutputSource, mapTerrainSource, rendererSource, appSource, mapPipelineSource, mapAdminStageSource, mapPatchSource, worldMapSource, municipalSource, testSource] = await Promise.all([ readLocalText("./names.js"), readLocalText("./mapGenerator.js"), readLocalText("./mapOutput.js"), readLocalText("./mapTerrain.js"), readLocalText("./renderer.js"), readLocalText("./app.js"), readLocalText("./mapPipeline.js"), readLocalText("./mapAdminStage.js"), readLocalText("./mapPatch.js"), readLocalText("./worldMap.js"), readLocalText("./mapMunicipalCoherence.js"), readLocalText("./test.js"), ]); function assert(condition, message) { if (condition) logLines.push(`OK: ${message}`); else { failed += 1; logLines.push(`NG: ${message}`); } } function terrainBoundaryTargetForMetrics(map, i) { const lu = map.landuse[i]; const urbanPenalty = Math.min(1, (lu === 3 ? 1.45 : lu === 2 ? 1.12 : lu === 4 ? 0.95 : lu === 7 ? 0.90 : lu === 8 ? 0.64 : lu === 5 || lu === 6 ? 0.48 : 0) + map.populationDensity[i] * 1.35); const majorRiver = Math.min(1, Math.max(map.river[i] - 0.32, 0) * 1.9 + Math.max(map.flowAccum[i] - 0.38, 0) * 0.75); const minorStream = Math.min(1, map.river[i] * 0.34 + map.flowAccum[i] * 0.18); const ridgeDivide = Math.min(1, map.ridgeField[i] * 1.55 + Math.max(0, map.elevation[i] - 0.54) * map.ridgeField[i] * 0.95); const slopeBreak = Math.min(1, map.slope[i] * 0.58 + Math.max(0, map.slope[i] - 0.32) * 0.68); const highGround = Math.max(0, map.elevation[i] - 0.56) * 0.22; const valleyFloorPenalty = map.valleyField[i] * (majorRiver > 0.34 ? -0.10 : -0.62); return Math.max(0, Math.min(1, ridgeDivide + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72)); } function adminBoundaryMetrics(map) { let borderEdges = 0; let targetSum = 0; let denseUrbanEdges = 0; let rightAngleRuns = 0; let voronoiLikeEdges = 0; let lowScoreFlatEdges = 0; for (let y = 1; y < MAP_H - 1; y++) { for (let x = 1; x < MAP_W - 1; x++) { const i = indexOf(x, y); if (!map.prefectureMask[i] || map.sea[i] || map.adminId[i] < 0) continue; for (const [dx, dy] of [[1, 0], [0, 1]]) { const ni = indexOf(x + dx, y + dy); if (!map.prefectureMask[ni] || map.sea[ni] || map.adminId[ni] < 0 || map.adminId[ni] === map.adminId[i]) continue; borderEdges++; const edgeTarget = (terrainBoundaryTargetForMetrics(map, i) + terrainBoundaryTargetForMetrics(map, ni)) * 0.5; targetSum += edgeTarget; const urban = Math.max(map.populationDensity[i], map.populationDensity[ni]) > 0.58 || [2, 3, 4, 7, 8].includes(map.landuse[i]) || [2, 3, 4, 7, 8].includes(map.landuse[ni]); if (urban) denseUrbanEdges++; const ca = map.adminCenters[map.adminId[i]]; const cb = map.adminCenters[map.adminId[ni]]; if (ca && cb) { const mx = x + dx * 0.5; const my = y + dy * 0.5; const dA = Math.hypot(mx - ca.x, my - ca.y); const dB = Math.hypot(mx - cb.x, my - cb.y); if (Math.abs(dA - dB) < 4.2 && edgeTarget < 0.40) voronoiLikeEdges++; } if (edgeTarget < 0.16 && Math.max(map.slope[i], map.slope[ni]) < 0.24 && Math.max(map.ridgeField[i], map.ridgeField[ni]) < 0.28 && Math.max(map.river[i], map.river[ni]) < 0.26) { lowScoreFlatEdges++; } const sideA = indexOf(x + (dy ? 1 : 0), y + (dx ? 1 : 0)); const sideB = indexOf(x - (dy ? 1 : 0), y - (dx ? 1 : 0)); if (map.prefectureMask[sideA] && map.prefectureMask[sideB] && !map.sea[sideA] && !map.sea[sideB]) { const turnA = map.adminId[sideA] !== map.adminId[i] && map.adminId[sideA] !== map.adminId[ni]; const turnB = map.adminId[sideB] !== map.adminId[i] && map.adminId[sideB] !== map.adminId[ni]; if ((turnA || turnB) && terrainBoundaryTargetForMetrics(map, i) < 0.46) rightAngleRuns++; } } } } const ids = new Set([...map.adminId].filter((id, i) => id >= 0 && map.prefectureMask[i] && !map.sea[i])); const areaById = new Map(); for (let i = 0; i < map.adminId.length; i++) { if (map.prefectureMask[i] && !map.sea[i] && map.adminId[i] >= 0) areaById.set(map.adminId[i], (areaById.get(map.adminId[i]) || 0) + 1); } const areas = [...areaById.values()].sort((a, b) => a - b); const medianArea = areas.length ? areas[Math.floor(areas.length / 2)] : 1; const maxArea = areas.length ? areas[areas.length - 1] : 1; let disconnectedMunicipalities = 0; let maxComponents = 0; const seen = new Uint8Array(MAP_W * MAP_H); for (const id of ids) { let comps = 0; seen.fill(0); for (let i = 0; i < map.adminId.length; i++) { if (seen[i] || map.adminId[i] !== id || !map.prefectureMask[i] || map.sea[i]) continue; comps++; const queue = [i]; seen[i] = 1; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; const x = cur % MAP_W; const y = Math.floor(cur / MAP_W); for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = x + dx; const ny = y + dy; if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue; const ni = indexOf(nx, ny); if (seen[ni] || map.adminId[ni] !== id || !map.prefectureMask[ni] || map.sea[ni]) continue; seen[ni] = 1; queue.push(ni); } } } if (comps > 1) disconnectedMunicipalities++; maxComponents = Math.max(maxComponents, comps); } const centerValidCount = map.adminCenters.filter((center) => { const i = indexOf(center.x, center.y); return map.prefectureMask[i] && !map.sea[i] && map.adminId[i] >= 0; }).length; return { borderEdges, avgTarget: borderEdges ? targetSum / borderEdges : 0, denseUrbanRate: borderEdges ? denseUrbanEdges / borderEdges : 0, rightAngleRate: borderEdges ? rightAngleRuns / borderEdges : 0, voronoiLikeRate: borderEdges ? voronoiLikeEdges / borderEdges : 0, lowScoreFlatRate: borderEdges ? lowScoreFlatEdges / borderEdges : 0, areaDiversity: maxArea / Math.max(1, medianArea), municipalityCount: ids.size, disconnectedMunicipalities, maxComponents, centerValidRatio: map.adminCenters.length ? centerValidCount / map.adminCenters.length : 1, }; } function majorCityCoreIntegrity(map) { const majorCities = map.modernCities.filter((city) => (city.population || 0) >= 180000); if (majorCities.length === 0) return 1; let sum = 0; let checked = 0; for (const city of majorCities) { const counts = new Map(); const r = Math.ceil(Math.max(3, city.coreRadius || 4)); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const x = city.x + dx; const y = city.y + dy; if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H || Math.hypot(dx, dy) > r) continue; const i = indexOf(x, y); if (!map.prefectureMask[i] || map.sea[i]) continue; if (map.landuse[i] !== 3 && map.populationDensity[i] < 0.38) continue; const id = map.adminId[i]; if (id >= 0) counts.set(id, (counts.get(id) || 0) + 1); } } const total = [...counts.values()].reduce((a, b) => a + b, 0); if (total === 0) continue; sum += Math.max(...counts.values()) / total; checked++; } return checked ? sum / checked : 1; } function satelliteMunicipalityMetrics(map) { const areaById = new Map(); for (let i = 0; i < map.adminId.length; i++) { if (map.prefectureMask[i] && !map.sea[i] && map.adminId[i] >= 0) areaById.set(map.adminId[i], (areaById.get(map.adminId[i]) || 0) + 1); } const rows = (map.satelliteCities || []) .filter((sat) => map.prefectureMask[indexOf(sat.x, sat.y)] && !map.sea[indexOf(sat.x, sat.y)]) .map((sat) => { const admin = map.adminId[indexOf(sat.x, sat.y)]; return { sat, admin, area: areaById.get(admin) || 0 }; }); const independent = rows.filter((row) => row.sat.municipalityClass === "independentSatelliteMunicipality"); const small = independent.filter((row) => row.area < 80); const largeTooSmall = rows.filter((row) => (row.sat.population || 0) >= 60000 && row.sat.municipalityClass === "independentSatelliteMunicipality" && row.area < 120); const average = independent.length ? independent.reduce((sum, row) => sum + row.area, 0) / independent.length : 0; return { rows, independent, small, largeTooSmall, average }; } function regionalComponentMetrics(map) { const ids = new Set([...map.prefectureRegionId].filter((id, i) => id >= 0 && !map.sea[i])); const seen = new Uint8Array(MAP_W * MAP_H); let maxComponents = 0; const areas = []; for (const id of ids) { seen.fill(0); let comps = 0; let area = 0; for (let i = 0; i < map.prefectureRegionId.length; i++) { if (!map.sea[i] && map.prefectureRegionId[i] === id) area++; if (seen[i] || map.sea[i] || map.prefectureRegionId[i] !== id) continue; comps++; const queue = [i]; seen[i] = 1; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; const x = cur % MAP_W; const y = Math.floor(cur / MAP_W); for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = x + dx; const ny = y + dy; if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue; const ni = indexOf(nx, ny); if (seen[ni] || map.sea[ni] || map.prefectureRegionId[ni] !== id) continue; seen[ni] = 1; queue.push(ni); } } } maxComponents = Math.max(maxComponents, comps); areas.push(area); } areas.sort((a, b) => a - b); const medianArea = areas.length ? areas[Math.floor(areas.length / 2)] : 0; const minArea = areas.length ? areas[0] : 0; const tinyCount = areas.filter((area) => area < 520).length; return { regionCount: ids.size, maxComponents, minArea, medianArea, tinyCount, areas }; } function regionalBorderMetrics(map) { let invalidSame = 0; let expected = 0; const ids = map.prefectureRegionId; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (map.sea[i] || ids[i] < 0) continue; if (x + 1 < MAP_W) { const ni = indexOf(x + 1, y); if (!map.sea[ni] && ids[ni] >= 0 && ids[ni] !== ids[i]) expected++; } if (y + 1 < MAP_H) { const ni = indexOf(x, y + 1); if (!map.sea[ni] && ids[ni] >= 0 && ids[ni] !== ids[i]) expected++; } } } for (const segment of map.regionalPrefectureBorders || []) { const [[x1, y1], [x2, y2]] = segment; let a = -1, b = -1; if (x1 === x2) { const x = x1; const y = Math.min(y1, y2); if (x > 0 && x < MAP_W && y >= 0 && y < MAP_H) { a = ids[indexOf(x - 1, y)]; b = ids[indexOf(x, y)]; } } else if (y1 === y2) { const x = Math.min(x1, x2); const y = y1; if (y > 0 && y < MAP_H && x >= 0 && x < MAP_W) { a = ids[indexOf(x, y - 1)]; b = ids[indexOf(x, y)]; } } if (a < 0 || b < 0 || a === b) invalidSame++; } return { invalidSame, expected, actual: (map.regionalPrefectureBorders || []).length }; } function borderHierarchyViolations(map) { let prefectureCutsMunicipality = 0; let municipalityCutsCompartment = 0; const compId = map.naturalCompartmentId; for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (map.sea[i]) continue; for (const [nx, ny] of [[x + 1, y], [x, y + 1]]) { if (nx >= MAP_W || ny >= MAP_H) continue; const ni = indexOf(nx, ny); if (map.sea[ni]) continue; if (map.prefectureRegionId[i] !== map.prefectureRegionId[ni] && map.adminId[i] === map.adminId[ni]) prefectureCutsMunicipality++; if (map.adminId[i] !== map.adminId[ni] && compId && compId[i] === compId[ni]) municipalityCutsCompartment++; } } } return { prefectureCutsMunicipality, municipalityCutsCompartment }; } function longStraightLowBarrierSegments(map, segments, minRun = 18) { const runs = new Map(); for (const seg of segments || []) { const [[x1, y1], [x2, y2]] = seg; const vertical = x1 === x2; const key = vertical ? `v:${x1}` : `h:${y1}`; const pos = vertical ? Math.min(y1, y2) : Math.min(x1, x2); if (!runs.has(key)) runs.set(key, []); runs.get(key).push({ pos, seg }); } let bad = 0; for (const rows of runs.values()) { rows.sort((a, b) => a.pos - b.pos); let start = 0; for (let k = 1; k <= rows.length; k++) { if (k < rows.length && rows[k].pos <= rows[k - 1].pos + 1.01) continue; const run = rows.slice(start, k); if (run.length >= minRun) { const natural = run.reduce((sum, row) => { const [[x1, y1], [x2, y2]] = row.seg; const sx = Math.min(Math.max(0, Math.floor((x1 + x2) / 2)), MAP_W - 1); const sy = Math.min(Math.max(0, Math.floor((y1 + y2) / 2)), MAP_H - 1); return sum + (map.naturalBarrierScore?.[indexOf(sx, sy)] || 0); }, 0) / run.length; if (natural < 0.18) bad++; } start = k; } } return bad; } function regionalEnclaveCount(map) { const ids = map.prefectureRegionId; const regionIds = [...new Set([...ids].filter((id, i) => id >= 0 && !map.sea[i]))]; let enclaves = 0; for (const id of regionIds) { const seen = new Uint8Array(MAP_W * MAP_H); for (let i = 0; i < ids.length; i++) { if (seen[i] || map.sea[i] || ids[i] !== id) continue; const queue = [i]; const cells = []; let touchesOutside = false; const neighbors = new Set(); seen[i] = 1; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; cells.push(cur); const x = cur % MAP_W; const y = Math.floor(cur / MAP_W); if (x === 0 || y === 0 || x === MAP_W - 1 || y === MAP_H - 1) touchesOutside = true; for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = x + dx; const ny = y + dy; if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue; const ni = indexOf(nx, ny); if (map.sea[ni]) { touchesOutside = true; continue; } if (ids[ni] !== id && ids[ni] >= 0) neighbors.add(ids[ni]); if (seen[ni] || ids[ni] !== id) continue; seen[ni] = 1; queue.push(ni); } } if (cells.length && !touchesOutside && neighbors.size === 1) enclaves++; } } return enclaves; } function cityMunicipalityAreaMetrics(map) { const areaById = new Map(); for (let i = 0; i < map.adminId.length; i++) { const id = map.adminId[i]; if (id >= 0 && !map.sea[i]) areaById.set(id, (areaById.get(id) || 0) + 1); } const rows = (map.modernCities || []) .filter((city) => (city.population || 0) >= 95000 && city.insidePrefecture && !map.sea[indexOf(city.x, city.y)]) .map((city) => { const admin = map.adminId[indexOf(city.x, city.y)]; const minArea = Math.min((city.population || 0) >= 450000 ? 780 : 520, Math.max(130, 95 + Math.sqrt(city.population || 0) * 0.72 + (city.urbanFootprintCells || 0) * 0.42)); return { city, admin, area: areaById.get(admin) || 0, minArea }; }); return { rows, tooSmall: rows.filter((row) => row.area + 1e-6 < row.minArea * 0.82) }; } function prefectureNameForTest(map, i) { const id = map.prefectureRegionId?.[i] ?? -1; return (map.prefectureRegions || []).find((region) => region.id === id)?.name || ""; } function meanField(map, fieldName, predicate) { let sum = 0; let count = 0; const field = map[fieldName]; for (let i = 0; i < field.length; i++) { if (!predicate(i)) continue; sum += field[i]; count++; } return count ? sum / count : 0; } function ridgeSinuosityMetric(map) { const centers = []; for (let y = 1; y < MAP_H - 1; y++) { let sum = 0; let weight = 0; for (let x = 1; x < MAP_W - 1; x++) { const i = indexOf(x, y); if (map.sea[i]) continue; const r = Math.max(0, map.ridgeField[i] - 0.36); sum += x * r; weight += r; } if (weight > 1.2) centers.push(sum / weight); } if (centers.length < 8) return 0; let turn = 0; let total = 0; for (let i = 2; i < centers.length; i++) { const a = centers[i - 1] - centers[i - 2]; const b = centers[i] - centers[i - 1]; turn += Math.abs(b - a); total += Math.abs(b) + Math.abs(a) + 0.01; } return turn / total; } function terrainCoreMetrics(map) { const land = [...map.elevation].map((_, i) => i).filter((i) => !map.sea[i]); const mountainCells = land.filter((i) => map.elevation[i] > 0.58 || map.ridgeField[i] > 0.42).length; const lowlandCells = land.filter((i) => map.plain[i] > 0.38 || map.depositionalLowland?.[i] > 0.24).length; const ridgeValues = land.map((i) => map.ridgeField[i]); const ridgeMean = ridgeValues.reduce((sum, value) => sum + value, 0) / Math.max(1, ridgeValues.length); const ridgeVariance = ridgeValues.reduce((sum, value) => sum + (value - ridgeMean) ** 2, 0) / Math.max(1, ridgeValues.length); const depositionTargetMean = meanField(map, "depositionField", (i) => !map.sea[i] && (map.coastalLowland[i] > 0.18 || map.basinField[i] > 0.22 || map.river[i] > 0.18 || map.flowAccum[i] > 0.24)); const depositionOtherMean = meanField(map, "depositionField", (i) => !map.sea[i] && map.coastalLowland[i] < 0.08 && map.basinField[i] < 0.12 && map.river[i] < 0.06 && map.flowAccum[i] < 0.12 && map.ridgeField[i] < 0.28); const riverValleyMean = meanField(map, "valleyField", (i) => !map.sea[i] && map.river[i] > 0.20); const nonRiverValleyMean = meanField(map, "valleyField", (i) => !map.sea[i] && map.river[i] <= 0.02); return { landCount: land.length, mountainRatio: mountainCells / Math.max(1, land.length), lowlandRatio: lowlandCells / Math.max(1, land.length), ridgeVariance, ridgeSinuosity: ridgeSinuosityMetric(map), depositionTargetMean, depositionOtherMean, riverValleyMean, nonRiverValleyMean, depositionSum: [...map.depositionField].reduce((sum, value) => sum + value, 0), alluvialMax: Math.max(...(map.alluvialFanField || [0])), deltaMax: Math.max(...(map.deltaField || [0])), }; } function requiredTransportNodes(map) { const nodes = []; const seen = new Set(); function add(p, reason) { if (!p) return; const key = `${p.x},${p.y}`; if (seen.has(key)) return; seen.add(key); nodes.push({ ...p, requiredTransportReason: reason }); } add(map.prefecturalCapital, "capital"); for (const gate of map.externalGateways || []) add(gate, "externalGateway"); for (const city of map.modernCities || []) if ((city.population || 0) >= 120000 || city.isPrefecturalCapital) add(city, "majorCity"); for (const port of map.ports || []) if (port.portClass === "major") add(port, "majorPort"); return nodes; } function transportConnectivityMetrics(map) { const paths = [ ...map.railways, ...map.branchRailways, ...map.externalRailways, ...map.nationalRoads, ...(map.ringRoads || []), ...map.expressways, ...map.externalRoads, ...map.externalExpressways, ]; const pathCells = new Set(); for (const path of paths) for (const [x, y] of path) pathCells.add(`${x},${y}`); const nodes = requiredTransportNodes(map); function nearestCell(node) { let best = null; let bestD = Infinity; for (const key of pathCells) { const [x, y] = key.split(",").map(Number); const d = Math.hypot(node.x - x, node.y - y); if (d < bestD) { bestD = d; best = key; } } return { key: best, distance: bestD }; } const seen = new Set(); const components = []; for (const key of pathCells) { if (seen.has(key)) continue; const queue = [key]; const component = new Set([key]); seen.add(key); for (let q = 0; q < queue.length; q++) { const [x, y] = queue[q].split(",").map(Number); for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]) { const nk = `${x + dx},${y + dy}`; if (!pathCells.has(nk) || seen.has(nk)) continue; seen.add(nk); component.add(nk); queue.push(nk); } } components.push(component); } const mapped = nodes.map((node) => ({ node, nearest: nearestCell(node), components: components .map((component, componentIndex) => ({ componentIndex, near: [...component].some((key) => { const [x, y] = key.split(",").map(Number); return Math.hypot(node.x - x, node.y - y) <= 7; }), })) .filter((item) => item.near) .map((item) => item.componentIndex), })); const reachable = mapped.filter((item) => item.components.length > 0); let largestRequiredComponent = 0; for (let componentIndex = 0; componentIndex < components.length; componentIndex++) { largestRequiredComponent = Math.max(largestRequiredComponent, reachable.filter((item) => item.components.includes(componentIndex)).length); } return { requiredCount: nodes.length, reachableCount: reachable.length, largestRequiredComponent, isolatedExternalGateways: mapped.filter((item) => item.node.requiredTransportReason === "externalGateway" && item.components.length === 0).length, isolatedMajorCities: mapped.filter((item) => item.node.requiredTransportReason === "majorCity" && item.components.length === 0).length, }; } try { const map = generateMap(12345); const other = generateMap(54321); const size = MAP_W * MAP_H; const urbanCellCount = [...map.landuse].filter((value) => value >= 2 && value <= 8).length; const cityPopulations = map.modernCities.map((city) => city.population || 0); const maxPopulation = Math.max(...cityPopulations); const minPopulation = Math.min(...cityPopulations); const landElevations = [...map.elevation].filter((_, i) => !map.sea[i]); const meanElevation = landElevations.reduce((sum, value) => sum + value, 0) / landElevations.length; const elevationStdDev = Math.sqrt(landElevations.reduce((sum, value) => sum + (value - meanElevation) ** 2, 0) / landElevations.length); const modernPaths = [ ...map.railways, ...map.branchRailways, ...map.externalRailways, ...(map.ringRailways || []), ...map.nationalRoads, ...(map.ringRoads || []), ...map.expressways, ...(map.ringExpressways || []), ...map.externalRoads, ...map.externalExpressways, ]; const endpointDegree = new Map(); for (const path of modernPaths) { if (path.length < 2) continue; for (const point of [path[0], path[path.length - 1]]) { const key = point.join(","); endpointDegree.set(key, (endpointDegree.get(key) || 0) + 1); } } const maxModernEndpointDegree = Math.max(0, ...endpointDegree.values()); let maxCoastalElevationStep = 0; for (let y = 1; y < MAP_H - 1; y++) { for (let x = 1; x < MAP_W - 1; x++) { const i = indexOf(x, y); if (map.sea[i]) continue; for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const ni = indexOf(x + dx, y + dy); if (map.sea[ni]) maxCoastalElevationStep = Math.max(maxCoastalElevationStep, Math.abs(map.elevation[i] - map.elevation[ni])); } } } let railExpressHighMountainCells = 0; for (const path of [...map.railways, ...map.branchRailways, ...(map.ringRailways || []), ...map.externalRailways, ...map.expressways, ...(map.ringExpressways || []), ...map.externalExpressways]) { for (const [x, y] of path) { const i = indexOf(x, y); if (map.elevation[i] > 0.82) railExpressHighMountainCells += 1; } } let trunkHighElevationCells = 0; for (const path of modernPaths) { for (const [x, y] of path) { if (map.elevation[indexOf(x, y)] > 0.72) trunkHighElevationCells += 1; } } const flatPlainCells = [...map.plain].filter((value, i) => !map.sea[i] && value > 0.72 && map.slope[i] < 0.12).length; const largeMountainCities = map.modernCities.filter((city) => { const i = indexOf(city.x, city.y); return (city.population || 0) >= 250000 && (map.elevation[i] > 0.66 || map.plain[i] < 0.18 || map.slope[i] > 0.88); }); const capitalInside = map.prefecturalCapital && map.prefectureMask[indexOf(map.prefecturalCapital.x, map.prefecturalCapital.y)]; const allNameable = map.entitiesForNames || []; const uniqueNames = new Set(allNameable.map((item) => item.name)); const duplicateNameRatio = allNameable.length ? 1 - uniqueNames.size / allNameable.length : 0; const activePoolChars = new Set(Object.values(NAME_KANJI_POOLS).flat().flatMap((part) => Array.from(String(part)))); const namedEntityCount = [ ...map.villages, ...map.ports, ...map.crossings, ...map.passes, ...map.markets, ...map.castles, ...map.castleTowns, ...map.modernCities, ...map.stations, ...map.industrialZones, ...map.interchanges, ...map.logisticsParks, ...map.satelliteCities, ...map.newTowns, ...map.castleRuins, ...map.externalGateways, ...map.adminCenters, ].filter((item) => item?.id && item?.name).length; const villageClusterMean = map.villages.length ? map.villages.reduce((sum, p) => sum + (map.settlementCluster?.[indexOf(p.x, p.y)] || 0), 0) / map.villages.length : 0; const meaningfulTransportNodes = [ ...map.modernCities, ...map.ports, ...map.markets, ...map.externalGateways, ...map.interchanges, ...map.industrialZones, ...map.logisticsParks, ]; const endpointPaths = [ ...map.railways, ...map.branchRailways, ...map.externalRailways, ...map.nationalRoads, ...map.expressways, ...map.externalRoads, ...map.externalExpressways, ...(map.icAccessRoads || []), ]; const endpointDistances = endpointPaths.flatMap((path) => path.length >= 2 ? [path[0], path[path.length - 1]] : []) .map(([x, y]) => Math.min(...meaningfulTransportNodes.map((p) => Math.hypot(p.x - x, p.y - y)))); const saneEndpointRatio = endpointDistances.length ? endpointDistances.filter((d) => d <= 10).length / endpointDistances.length : 1; const adminMetrics = adminBoundaryMetrics(map); const cityCoreIntegrity = majorCityCoreIntegrity(map); const satelliteMetrics = satelliteMunicipalityMetrics(map); const regionalMetrics = regionalComponentMetrics(map); const terrainMetrics = terrainCoreMetrics(map); const transportMetrics = transportConnectivityMetrics(map); assert(NAME_KANJI_POOLS && Array.isArray(NAME_KANJI_POOLS.modifiers), "NAME_KANJI_POOLS exists"); assert(NAME_TEMPLATES && NAME_TEMPLATES.modifierTerrain?.slots?.length === 2, "NAME_TEMPLATES exists"); assert(NAME_TEMPLATE_WEIGHTS && NAME_TEMPLATE_WEIGHTS.generic?.modifierTerrain > 0, "NAME_TEMPLATE_WEIGHTS exists"); assert(NAME_PROBABILITIES && NAME_PROBABILITIES.contextCategoryWeights?.generic, "NAME_PROBABILITIES exists"); const removedContextModule = "placeName" + "Context.js"; assert(!namesSource.includes(removedContextModule) && !mapGeneratorSource.includes(removedContextModule) && !testSource.includes(removedContextModule), "removed name-context import is absent"); assert(Object.keys(NAME_KANJI_POOLS).every((key) => Array.isArray(NAME_KANJI_POOLS[key])), "name category pools are centralized arrays"); assert(Object.values(NAME_KANJI_POOLS).every((pool) => pool.every((part) => typeof part === "string" && !part.includes("\uFFFD"))), "configured name category pools contain valid strings"); assert(Object.keys(NAME_PARTS).length === 0, "legacy NAME_PARTS has no hidden candidates"); const removedContextSuffixConst = "CONTEXT" + "_SUFFIXES"; const removedContextSuffixKey = "context" + "Suffixes"; assert(!namesSource.includes(removedContextSuffixConst) && !namesSource.includes(removedContextSuffixKey), "hidden context suffix arrays are absent"); assert(!/export\s+const\s+NAME_PROBABILITIES[\s\S]*export\s+const\s+NAME_PROBABILITIES/.test(namesSource), "NAME_PROBABILITIES has one source"); assert(mapPatchSource.includes("splitWorldPathByPatch") && mapPatchSource.includes("patchAffected"), "patch path merging is alpha-aware for lasso selections"); assert(!mapPatchSource.includes("patchAlpha(x, y, rects, 0)"), "patch admin repair uses the active patch seed"); assert(mapPatchSource.includes("refreshPatchInfluenceFields") && mapPatchSource.includes("roadCellsPainted"), "patch generation refreshes derived transport influence fields after path merges"); assert(mapPatchSource.includes("normalizeGeneratedPointIds"), "patch generation normalizes generated point admin and prefecture ids"); assert(mapPatchSource.includes("generateMap(seed") && mapPatchSource.includes('patchGenerationMode = "legacy-full-pipeline"'), "patch generation remains full-pipeline simulation"); assert(mapPatchSource.includes("patchTimings") && appSource.includes("result.patchTimings"), "patch generation returns and renders timing rows"); assert(mapPatchSource.includes("PATCH_CANDIDATE_CACHE_LIMIT") && mapPatchSource.includes("patchCandidateCacheKey") && mapPatchSource.includes("cache.size > PATCH_CANDIDATE_CACHE_LIMIT"), "patch candidate cache is bounded and keyed"); assert(mapPatchSource.includes("getPatchAlphaCache") && mapPatchSource.includes("getPatchSourceIndexCache"), "patch generation caches alpha and source-index grids for merge work"); assert(mapPatchSource.includes("const searchRect = expandRect(rect, 16, world)") && mapPatchSource.includes("connectorAttempts"), "patch connector pathfinding uses bounded attempts and a shared search rect"); assert(worldMapSource.includes("shiftSelectionShape") && worldMapSource.includes("selectionShape = shiftSelectionShape"), "world expansion shifts stored lasso patch polygons"); assert(municipalSource.includes("reconcileMunicipalMetadata") && mapOutputSource.includes("reconcileMunicipalMetadata") && mapPatchSource.includes("reconcileMunicipalMetadata"), "municipal metadata is reconciled in output and patch repair"); assert(appSource.includes("mappedPref === id") && !appSource.includes("return nearestNamedAdminCenter(map, cellIndex, maxDistance, null)"), "tooltip municipal fallback requires exact coherent ids"); assert(mapPatchSource.includes("signedDist") && mapPatchSource.includes("patchBand"), "lasso patch alpha uses a feathered signed seam band"); assert(mapPatchSource.includes("repairDiscreteSeamOwnership") && mapPatchSource.includes("chooseSeamOwnerValue"), "patch admin and prefecture seams use ownership repair"); assert(mapPatchSource.includes("featherTerrainSeam") && mapPatchSource.includes("terrainFeatherCells"), "patch terrain transition bands are feather-smoothed"); assert(mapPatchSource.includes("strongOnly") && mapPatchSource.includes("patchAlpha(x, y, rects, seed)"), "patch water topology avoids weak low-alpha seam flips"); assert(!municipalSource.includes("Municipality ${id + 1}") && !municipalSource.includes("Prefecture ${id + 1}") && municipalSource.includes("自治${id + 1}") && municipalSource.includes("県域${id + 1}"), "fallback municipal and prefecture metadata avoids generic English labels"); assert(map.elevation.length === size, "elevation length matches map size"); assert(map.sea.length === size, "sea length matches map size"); assert(map.ocean.length === size && map.lake.length === size, "ocean and lake masks match map size"); assert(map.river.length === size, "river length matches map size"); assert(map.landuse.length === size, "land-use length matches map size"); assert(map.adminId.length === size, "municipal id length matches map size"); assert(map.prefectureMask.length === size, "prefecture mask length matches map size"); assert(map.populationDensity.length === size, "population density length matches map size"); assert(map.ridgeField.length === size && map.valleyField.length === size && map.flowAccum.length === size, "causal terrain fields match map size"); assert(map.erosionField.length === size && map.depositionField.length === size, "erosion and deposition fields match map size"); assert(map.arcSpineField.length === size && map.branchRidgeField.length === size, "spine and branch ridge fields match map size"); assert(map.depositionalLowland.length === size && map.alluvialFanField.length === size && map.deltaField.length === size, "depositional debug fields match map size"); assert(map.naturalBarrierScore.length === size, "natural barrier score field matches map size"); assert(map.terrainTemplate && Number.isFinite(map.terrainTemplate.deposition) && Number.isFinite(map.terrainTemplate.erosion), "terrain template parameters are exposed"); assert(map.terrainDebug && Number.isFinite(map.terrainDebug.primarySpineStrength), "terrain debug metrics exist"); assert(map.terrainDebug.primarySpineStrength > 0.08, "primary mountain spine has visible strength"); assert(map.terrainDebug.largeInlandLakeCount <= 1, "large inland lakes are rare"); assert(map.terrainDebug.smallIslandCount <= 24, "small island/coast speckles stay limited"); assert(map.terrainDebug.depositionLowlandArea > 0, "depositional lowland area is tracked"); assert(["east-west", "north-south", "diagonal"].includes(map.terrainTemplate.coastAxis) && map.terrainTemplate.coastSides?.length === 2, "paired coast template parameters are exposed"); assert(map.settlementCluster.length === size, "settlement cluster field matches map size"); assert(map.prefectureRegionId.length === size && Array.isArray(map.regionalPrefectureBorders), "neighbor prefecture regions exist"); assert(map.prefectureRegionId.length === size, "final prefecture id field matches map size"); assert(map.naturalCompartmentId?.length === size && Array.isArray(map.naturalCompartments), "shared natural compartments are exposed"); assert(map.adminDebug?.naturalCompartmentCount > 0 && map.adminDebug?.finalMunicipalityCount > 0, "natural compartments are generated before municipalities"); assert(map.regionalDebug?.prefecturesGeneratedAfterMunicipalities === true && map.regionalDebug?.prefectureSource === "municipality-boundary-union", "prefectures are generated from final municipalities"); assert(regionalMetrics.regionCount >= 4 && regionalMetrics.maxComponents === 1, "each non-sea prefecture region is connected after repair"); assert(regionalEnclaveCount(map) === 0, "final prefecture regions contain no one-region enclosed enclaves"); const regionalBorders = regionalBorderMetrics(map); const hierarchyViolations = borderHierarchyViolations(map); assert(hierarchyViolations.prefectureCutsMunicipality === 0, "no prefecture border cuts through a municipality"); assert(hierarchyViolations.municipalityCutsCompartment === 0, "no municipality border cuts through a natural compartment"); assert(regionalBorders.invalidSame === 0 && regionalBorders.actual === regionalBorders.expected, "rendered prefecture borders separate different final prefecture ids only"); assert(map.regionalDebug.finalRegionalPrefectureBorderCount === map.regionalPrefectureBorders.length, "regional prefecture borders are final output borders"); assert(!mapTerrainSource.includes("generateRegionalPrefectures") && !mapPipelineSource.includes("prefectureRegionId, sea") && mapAdminStageSource.includes("generatePrefecturesFromMunicipalities"), "administrative order is natural compartments to municipalities to prefectures"); assert(![mapTerrainSource, mapPipelineSource, mapAdminStageSource, mapOutputSource, rendererSource, appSource].some((source) => /displayRegionId|adminPrefectureRegionId/.test(source)), "prefecture pipeline does not use display/admin-prefecture id aliases"); assert(!("maritimePrefectureBorders" in map), "maritime prefecture borders are not emitted"); assert(!mapOutputSource.includes("maritimePrefectureBorders") && !rendererSource.includes("maritimePrefectureBorders"), "maritime prefecture borders are not generated or rendered"); assert(rendererSource.includes("drawPrefectureRegionFill") && rendererSource.includes("map.prefectureRegionId"), "prefecture fill renderer uses final prefecture id source"); assert(regionalMetrics.tinyCount <= Math.max(1, Math.floor(regionalMetrics.regionCount * 0.12)) && regionalMetrics.medianArea >= 1200 && regionalMetrics.minArea >= 520, "regional prefectures avoid excessive tiny slivers"); assert(Array.isArray(map.prefectureRegions) && map.prefectureRegions.length === regionalMetrics.regionCount, "prefecture region metadata exists for every region"); assert(map.prefectureRegions.every((region) => region.name && Number.isFinite(region.x) && Number.isFinite(region.y) && region.area > 0 && map.prefectureRegionId[indexOf(region.x, region.y)] === region.id), "every prefecture region has a name and valid label point"); assert(map.regionalDebug.finalRegionalMaxAreaShare < 0.38, "no single prefecture dominates regional land area"); assert(longStraightLowBarrierSegments(map, map.regionalPrefectureBorders, 22) === 0, "prefecture borders avoid long straight low-barrier cuts"); assert(longStraightLowBarrierSegments(map, map.adminBorders, 20) === 0, "municipality borders avoid long straight low-barrier cuts"); assert(Array.isArray(map.tributaryRivers) && Array.isArray(map.smallStreams), "river hierarchy arrays exist"); assert(Array.isArray(map.icAccessRoads), "IC access road array exists"); assert(Array.isArray(map.satelliteCities), "satelliteCities is an array"); assert(Array.isArray(map.ringRoads) && Array.isArray(map.ringExpressways) && Array.isArray(map.ringRailways), "ring transport arrays exist"); assert(Array.isArray(map.mainRivers), "mainRivers is an array"); assert(Array.isArray(map.minorRoads), "minorRoads is an array"); assert(Array.isArray(map.externalGateways), "externalGateways is an array"); let prefectureComponents = 0; const seenPrefecture = new Uint8Array(size); for (let i = 0; i < size; i++) { if (!map.prefectureMask[i] || seenPrefecture[i]) continue; prefectureComponents++; const queue = [i]; seenPrefecture[i] = 1; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; const x = cur % MAP_W; const y = Math.floor(cur / MAP_W); for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue; const ni = ny * MAP_W + nx; if (!map.prefectureMask[ni] || seenPrefecture[ni]) continue; seenPrefecture[ni] = 1; queue.push(ni); } } } } assert(prefectureComponents === 1, "prefecture area is a single connected component"); assert(map.prefectureBorder.length > 0, "prefecture border exists"); assert([...map.ocean].some((value) => value === 1), "edge-connected ocean mask exists"); assert([...map.lake].every((value, i) => !value || (map.sea[i] && !map.ocean[i])), "lake mask only marks isolated non-ocean water"); assert([...map.sea].every((value, i) => !value || map.ocean[i] || map.lake[i]), "water cells are classified as ocean or lake"); assert(map.adminBorders.length > 0, "municipal borders exist"); assert(map.mainRivers.length > 0, "at least one major river exists"); assert(map.tributaryRivers.length > 0, "tributary river network exists"); assert(map.smallStreams.length > 0, "small stream network exists"); assert(terrainMetrics.mountainRatio > 0.10 && terrainMetrics.mountainRatio < 0.72, "mountain and ridge area is meaningful but not total"); assert(terrainMetrics.lowlandRatio > 0.08 && terrainMetrics.lowlandRatio < 0.72, "lowlands exist without dominating every map"); assert(terrainMetrics.ridgeVariance > 0.004, "ridge field has nontrivial spatial variance"); assert(terrainMetrics.ridgeSinuosity > 0.015, "ridge centerlines are not perfectly straight bands"); assert(terrainMetrics.depositionSum > 0.2, "deposition field has nonzero values"); assert(terrainMetrics.depositionTargetMean >= terrainMetrics.depositionOtherMean * 0.85, "deposition favors rivers, basins, and coastal lowlands"); assert(terrainMetrics.riverValleyMean > terrainMetrics.nonRiverValleyMean * 1.08, "river cells overlap valley fields more than random non-river cells"); assert(terrainMetrics.alluvialMax > 0 || terrainMetrics.deltaMax > 0, "alluvial fan or delta fields are active"); assert(map.ports.some((p) => p.portClass === "major"), "at least one major port is classified"); assert(map.ports.every((p) => ["major", "regional", "fishing", "lake"].includes(p.portClass)), "ports have explicit classes"); assert(map.externalGateways.length > 0, "external gateways exist"); assert(map.minorRoads.length > 0, "minor roads exist"); assert(map.adminCenters.length >= 18, "municipality center count is sufficiently large"); const activeMunicipalityIds = new Set([...map.adminId].filter((id, i) => id >= 0 && !map.sea[i])); const centerIds = new Set(map.adminCenters.map((center) => center.adminId ?? center.municipalityId ?? center.adminNumericId).filter((id) => Number.isFinite(id))); assert(activeMunicipalityIds.size === map.adminCenters.length && [...activeMunicipalityIds].every((id) => centerIds.has(id)), "every active municipality has exactly one municipal center"); assert(map.adminCenters.every((center) => activeMunicipalityIds.has(center.adminId ?? center.municipalityId ?? center.adminNumericId)), "municipal centers do not point to inactive municipalities"); assert([...activeMunicipalityIds].every((id) => map.municipalityToPrefectureId?.[id] >= 0), "every active municipality maps to a prefecture"); const activePrefectureIds = new Set([...map.prefectureRegionId].filter((id, i) => id >= 0 && !map.sea[i])); const prefMetadataIds = new Set((map.prefectureRegions || []).map((region) => region.id)); assert([...activePrefectureIds].every((id) => prefMetadataIds.has(id)), "every active prefecture id has metadata"); assert(adminMetrics.municipalityCount >= 10, "terrain snapping preserves a reasonable municipality count"); assert(adminMetrics.centerValidRatio >= 0.95, "municipality centers remain on valid assigned land cells"); assert(adminMetrics.maxComponents <= 4, "municipal topology repair prevents excessive disconnected fragments"); assert(adminMetrics.disconnectedMunicipalities <= Math.max(2, Math.ceil(adminMetrics.municipalityCount * 0.20)), "most municipalities remain connected after terrain snapping"); assert(adminMetrics.avgTarget > 0.18, "admin borders align with terrain target features often enough"); assert(map.adminDebug && map.adminDebug.compartmentCount > 0, "natural compartment debug is available"); assert(map.adminDebug.averageCompartmentArea > 0, "natural compartments have positive average area"); assert(Number.isFinite(map.adminDebug.changedAfterLandscapePartition) && Number.isFinite(map.adminDebug.changedAfterSnap), "municipal changed-cell diagnostics exist"); assert(map.adminDebug.changedAfterCompartmentAssignment > 0 || map.adminDebug.changedAfterLandscapePartition > 0 || map.adminDebug.changedAfterSnap > 0, "municipal compartment or terrain passes change admin cells"); assert(map.adminDebug.changedAfterFinalExclaveRemoval + map.adminDebug.changedAfterFinalMerge < Math.max(2800, (map.adminDebug.changedAfterLandscapePartition + map.adminDebug.changedAfterSnap + map.adminDebug.changedAfterUrbanLock) * 1.35), "final municipal repair does not erase most terrain and urban changes"); assert(map.adminDebug.finalBorderNaturalBarrierAverage >= 0, "natural barrier score is tracked along final borders"); assert(map.adminDebug.voronoiLikeRateAfter <= Math.max(0.72, map.adminDebug.voronoiLikeRateBefore + 0.20), "natural compartment pass does not increase weak bisectors excessively"); assert(Number.isFinite(map.adminDebug.satelliteMunicipalitiesCreated) && Number.isFinite(map.adminDebug.averageSatelliteMunicipalityArea), "satellite municipality debug is available"); assert(satelliteMetrics.independent.length < 3 || satelliteMetrics.small.length / satelliteMetrics.independent.length <= 0.35, "tiny independent satellite municipalities are not the dominant pattern"); assert(satelliteMetrics.largeTooSmall.length === 0, "large independent satellites have meaningful municipal area"); assert(satelliteMetrics.independent.length < 3 || satelliteMetrics.average >= 140, "average independent satellite municipality area is meaningful"); assert(satelliteMetrics.rows.every((row) => row.area >= 80 || row.sat.municipalityClass === "smallTownAttachedToRuralMunicipality" || row.sat.municipalityClass === "suburbanDistrictMergedWithParent" || row.sat.municipalityClass === "newTownDistrict"), "tiny satellite areas are merged or explicitly classified as attached districts"); assert(adminMetrics.denseUrbanRate < 0.42, "admin borders avoid excessive dense urban crossings"); assert(adminMetrics.rightAngleRate < 0.46, "admin borders avoid excessive unsupported stair-step artifacts"); assert(adminMetrics.voronoiLikeRate < 0.58, "admin borders are not dominated by weak-terrain center bisectors"); assert(adminMetrics.lowScoreFlatRate < 0.40, "admin borders avoid excessive low-score flat-plain cuts"); assert(adminMetrics.areaDiversity > 1.45, "municipality areas retain natural size diversity"); assert(cityCoreIntegrity >= 0.62, "major city cores remain mostly inside one municipality"); assert(urbanCellCount > 1000, "large-city urbanized cells are broad enough"); const cbdCells = [...map.landuse].filter((value) => value === 3).length; assert(cbdCells > 0, "CBD is represented as land-use cells rather than markers"); assert(map.modernCities.every((city) => Number.isFinite(city.population) && city.population > 0), "modern cities have population properties"); assert(maxPopulation / Math.max(1, minPopulation) > 3, "city populations vary strongly"); assert(map.totalPopulation >= cityPopulations.reduce((sum, value) => sum + value, 0), "total population includes city and satellite populations"); assert(Math.max(...map.populationDensity) > 0.9, "population density is normalized and populated"); assert([...map.populationDensity].some((value, i) => value === 0 && !map.sea[i]), "valid land cells can retain exactly zero population density"); assert(elevationStdDev > 0.18, "terrain relief has sufficient contrast"); assert(maxCoastalElevationStep < 0.12, "coastline and elevation do not create cliff artifacts"); assert(railExpressHighMountainCells === 0, "railways and expressways avoid huge mountain cells"); assert(trunkHighElevationCells === 0, "trunk roads, railways, and expressways avoid high-elevation cells"); assert(flatPlainCells >= 160, "broad flat plains exist as actual low-slope cells"); assert(largeMountainCities.length === 0, "large cities are not placed on unsuitable mountain sites"); assert(capitalInside, "prefectural capital is inside the prefecture"); assert(maxModernEndpointDegree <= 10, "modern transport endpoints are not over-centralized"); assert(map.entitiesForNames.every((item) => item.id && item.name), "nameable entities have ids and names"); assert(map.adminCenters.every((item) => item.id && item.name), "municipal centers have ids and names"); assert(map.entitiesForNames.some((item) => item.kind === "Municipal Center"), "municipal centers are included in label/name candidates"); assert(map.adminCenters.every((item) => item.name && Array.from(String(item.name)).length >= 2), "municipal center names are valid labels"); assert(map.adminCenters.some((item) => item.representativeFeatureName), "municipal centers keep representative feature metadata when available"); assert(map.adminCenters.every((item) => Array.from(String(item.name)).length >= 2), "municipal center names are not one-character labels"); assert(map.adminCenters.every((item) => !(item.representativeFeatureName && String(item.name).startsWith(item.representativeFeatureName) && Array.from(String(item.name)).length === Array.from(String(item.representativeFeatureName)).length + 1)), "municipal names avoid dangling one-kanji suffix fallback"); const cityMunicipalityMetrics = cityMunicipalityAreaMetrics(map); assert(cityMunicipalityMetrics.tooSmall.length === 0, "meaningful populated cities keep population-scaled municipality area"); const hoverCell = map.prefectureRegions.find((region) => region.area > 0); assert(hoverCell && prefectureNameForTest(map, indexOf(hoverCell.x, hoverCell.y)) === hoverCell.name && appSource.includes("Prefecture:") && appSource.includes("prefectureNameForCell"), "tooltip can resolve prefecture name for a hovered cell"); assert(appSource.includes("maxLeft") && appSource.includes("maxTop"), "tooltip position is clamped inside map container"); const adminNameDuplicateRatio = map.adminCenters.length ? 1 - new Set(map.adminCenters.map((item) => item.name)).size / map.adminCenters.length : 0; assert(adminNameDuplicateRatio < 0.22, "duplicate municipal names stay low"); assert(map.adminDebug.naturalCompartmentCount > adminMetrics.municipalityCount, "natural compartments are finer than municipalities"); assert(map.adminDebug.targetMunicipalityCount >= 20 && map.adminDebug.targetMunicipalityCount <= 50 && map.adminDebug.actualMunicipalityCount >= 18, "municipality target and actual counts are dense enough"); assert(map.adminDebug.changedAfterCompartmentAssignment > 0, "municipal compartment assignment is active"); assert(map.adminDebug.seedCellRevivalCount === 0, "seed cell revival is disabled"); assert(map.adminDebug.candidateSeedCount >= map.adminDebug.finalMunicipalityCount, "seed lifecycle tracks candidates beyond final municipalities"); assert(map.adminDebug.absorbedSeedCount >= 0 && map.adminDebug.pendingSeedCount === 0, "unresolved pending seeds are absorbed"); assert(map.adminDebug.finalTinyMunicipalityCount <= Math.max(3, Math.ceil(map.adminDebug.finalMunicipalityCount * 0.16)), "tiny municipalities remain a small fraction"); assert(Array.isArray(map.adminDebug.compartmentBorders) && map.adminDebug.compartmentBorders.length > map.adminBorders.length, "natural compartment borders are available for debug rendering"); assert(map.entitiesForNames.every((item) => typeof item.name === "string"), "nameable entity names are strings"); assert(map.entitiesForNames.every((item) => !String(item.name).includes("\uFFFD")), "generated names contain no replacement characters"); assert(map.entitiesForNames.every((item) => Array.from(String(item.name)).length >= 2), "one-character generated names are prevented"); assert(map.entitiesForNames.every((item) => validateGeneratedName(item.name, { allowAsciiDiagnostic: true }).valid), "generated names pass place-name validation"); assert(map.entitiesForNames.every((item) => String(item.name).length > 0), "empty generated names are prevented"); assert(duplicateNameRatio < 0.18, "generated place-name duplicates stay low"); assert(map.nameDebug && Array.isArray(map.nameDebug.emptyPools), "nameDebug reports empty pools"); assert(map.nameDebug.oneKanjiAppendFallbackUsed === 0, "one-kanji append fallback is never used"); assert((map.nameDebug.derivedNameCount || 0) === 0, "derived municipality suffix names are not generated"); assert((map.nameDebug.maxDerivedPerBase || 0) <= 2, "derived names per base stay small"); assert(map.nameDebug.emptyPools.length === Object.values(NAME_KANJI_POOLS).filter((pool) => pool.length === 0).length, "nameDebug empty pools match configured pools"); assert(map.nameDebug.selectedTemplateCounts && typeof map.nameDebug.selectedTemplateCounts === "object", "nameDebug selectedTemplateCounts exists"); assert(map.nameDebug.selectedContextCounts && typeof map.nameDebug.selectedContextCounts === "object", "nameDebug selectedContextCounts exists"); assert( map.nameDebug.generatedNamesUsed + map.nameDebug.customNameListUsed + map.nameDebug.fallbackAttempts === namedEntityCount, "nameDebug accounting covers named entities" ); assert(activePoolChars.size > 0 || generateTemplateName(777, "probe-0", { x: 10, y: 10, kind: "Probe" }, {}, 0, new Set()) === null, "template generation depends on active pools"); assert(villageClusterMean > 0.16, "villages prefer clustered valley, basin, coastal, and agricultural cells"); assert(saneEndpointRatio >= 0.76, "transport endpoints stay near meaningful generated nodes"); assert( map.adminCenters.length !== other.adminCenters.length || map.villages.length !== other.villages.length || map.markets.length !== other.markets.length, "feature counts vary between seeds" ); const againA = generateMap(999); const againB = generateMap(999); assert(JSON.stringify(againA.modernCities) === JSON.stringify(againB.modernCities), "generation is deterministic for the same seed"); assert(JSON.stringify(againA.entitiesForNames.map((item) => [item.id, item.name])) === JSON.stringify(againB.entitiesForNames.map((item) => [item.id, item.name])), "generated names are deterministic for the same seed"); assert(JSON.stringify(againA.adminCenters.map((item) => [item.id, item.x, item.y, item.name])) === JSON.stringify(againB.adminCenters.map((item) => [item.id, item.x, item.y, item.name])), "municipal centers are deterministic for the same seed"); assert(JSON.stringify([...againA.adminId]) === JSON.stringify([...againB.adminId]), "municipal adminId snapping is deterministic for the same seed"); assert(JSON.stringify([...againA.naturalCompartmentId]) === JSON.stringify([...againB.naturalCompartmentId]), "natural compartments are deterministic for the same seed"); assert(JSON.stringify([...againA.prefectureRegionId]) === JSON.stringify([...againB.prefectureRegionId]), "regional prefecture ids are deterministic for the same seed"); assert(JSON.stringify([...againA.municipalityToPrefectureId]) === JSON.stringify([...againB.municipalityToPrefectureId]), "municipality-to-prefecture ids are deterministic for the same seed"); assert(JSON.stringify(againA.regionalPrefectureBorders) === JSON.stringify(againB.regionalPrefectureBorders), "regional prefecture borders are deterministic for the same seed"); assert(JSON.stringify(againA.adminDebug) === JSON.stringify(againB.adminDebug), "admin debug metrics are deterministic for the same seed"); assert(JSON.stringify(againA.regionalDebug) === JSON.stringify(againB.regionalDebug), "regional debug metrics are deterministic for the same seed"); assert(JSON.stringify(againA.transportDebug) === JSON.stringify(againB.transportDebug), "transport debug metrics are deterministic for the same seed"); assert(JSON.stringify(transportConnectivityMetrics(againA)) === JSON.stringify(transportConnectivityMetrics(againB)), "transport connectivity metrics are deterministic for the same seed"); assert(JSON.stringify([...againA.elevation]) === JSON.stringify([...againB.elevation]), "elevation is deterministic for the same seed"); assert(JSON.stringify([...againA.ridgeField]) === JSON.stringify([...againB.ridgeField]), "ridge field is deterministic for the same seed"); assert(JSON.stringify([...againA.river]) === JSON.stringify([...againB.river]), "river field is deterministic for the same seed"); assert(JSON.stringify(terrainCoreMetrics(againA)) === JSON.stringify(terrainCoreMetrics(againB)), "terrain debug metrics are deterministic for the same seed"); const blockedCapitalName = "\u52A0\u8302"; const capitalNameMaps = [114514, 12345, 54321, 777, 999].map((seedValue) => generateMap(seedValue)); const capitalNames = capitalNameMaps.map((seeded) => seeded.prefecturalCapital?.name).filter(Boolean); assert(new Set(capitalNames).size > 1, "prefectural capital names vary across seeds"); assert(capitalNames.some((name) => name !== blockedCapitalName), "prefectural capital is not always the repeated custom name"); const semeMap = generateMap(8363712); const semeAdmin = (semeMap.adminCenters || []).find((center) => center.canonicalSettlementName); assert(!semeAdmin || semeAdmin.name === semeAdmin.canonicalSettlementName, "seed 8363712: municipality label uses canonical settlement name"); for (const [n, seeded] of capitalNameMaps.entries()) { const seedValue = [114514, 12345, 54321, 777, 999][n]; const metrics = terrainCoreMetrics(seeded); assert(seeded.mainRivers.length > 0 && seeded.tributaryRivers.length > 0 && seeded.smallStreams.length > 0, `seed ${seedValue}: river hierarchy exists`); assert(metrics.mountainRatio > 0.08 && metrics.lowlandRatio > 0.06, `seed ${seedValue}: mountain and lowland terrain both exist`); assert(metrics.ridgeVariance > 0.003 && metrics.ridgeSinuosity > 0.010, `seed ${seedValue}: ridges have varied jagged structure`); assert(metrics.depositionSum > 0.1 && metrics.depositionTargetMean >= metrics.depositionOtherMean * 0.75, `seed ${seedValue}: deposition is active in plausible lowlands`); assert(metrics.riverValleyMean > metrics.nonRiverValleyMean, `seed ${seedValue}: rivers follow valley fields`); assert(seeded.terrainDebug?.primarySpineStrength > 0.08, `seed ${seedValue}: primary spine is strong enough`); assert((seeded.terrainDebug?.largeInlandLakeCount || 0) <= 1, `seed ${seedValue}: large inland lakes are rare`); assert((seeded.terrainDebug?.smallIslandCount || 0) <= 24, `seed ${seedValue}: small island speckles are limited`); assert(seeded.villages.length > 0 && seeded.markets.length > 0 && seeded.modernCities.length > 0, `seed ${seedValue}: settlements are generated`); assert(seeded.premodernRoads.length > 0 && seeded.railways.length > 0, `seed ${seedValue}: roads and railways are generated`); const seededTransport = transportConnectivityMetrics(seeded); assert(seeded.adminId.length === size && seeded.adminBorders.length > 0 && seeded.regionalPrefectureBorders.length > 0, `seed ${seedValue}: admin and regional borders exist`); assert(seeded.adminCenters.every((item) => item.name && Array.from(String(item.name)).length >= 2), `seed ${seedValue}: every admin center has a valid name`); assert(seeded.entitiesForNames.some((item) => item.kind === "Municipal Center"), `seed ${seedValue}: admin labels are included in label candidates`); assert(seeded.adminCenters.every((item) => !(item.representativeFeatureName && String(item.name).startsWith(item.representativeFeatureName) && Array.from(String(item.name)).length === Array.from(String(item.representativeFeatureName)).length + 1)), `seed ${seedValue}: no dangling one-kanji admin suffix fallback`); assert(seeded.nameDebug?.oneKanjiAppendFallbackUsed === 0, `seed ${seedValue}: one-kanji append fallback stays unused`); assert((seeded.nameDebug?.derivedNameCount || 0) === 0, `seed ${seedValue}: derived suffix names stay disabled`); const seededAdminMetrics = adminBoundaryMetrics(seeded); const debug = seeded.adminDebug || {}; assert(seededAdminMetrics.municipalityCount >= 18 && seededAdminMetrics.municipalityCount <= 50, `seed ${seedValue}: municipality count stays in target range`); assert(debug.naturalCompartmentCount >= debug.actualMunicipalityCount * 2.5, `seed ${seedValue}: natural compartments are substantially finer than municipalities`); assert(debug.naturalCompartmentCount <= debug.actualMunicipalityCount * 10, `seed ${seedValue}: natural compartments do not become noisy cells`); assert(debug.averageCompartmentsPerMunicipality >= 2.5, `seed ${seedValue}: municipalities group multiple compartments on average`); assert(debug.singleCompartmentMunicipalityRatio < 0.35, `seed ${seedValue}: one-compartment municipalities are uncommon`); assert(debug.seedCellRevivalCount === 0, `seed ${seedValue}: seed cells are not revived after absorption`); assert(debug.finalMunicipalityCount >= 18 && debug.finalMunicipalityCount <= 50, `seed ${seedValue}: final municipality count remains bounded`); assert(debug.finalTinyMunicipalityCount <= Math.max(3, Math.ceil(debug.finalMunicipalityCount * 0.16)), `seed ${seedValue}: tiny municipalities stay uncommon`); assert(debug.pendingSeedsUsedForLowlandSplit > 0 || debug.absorbedSeedCount > 0 || debug.finalMunicipalityCount >= Math.min(20, debug.targetMunicipalityCount), `seed ${seedValue}: pending seeds are either used for lowland splits or absorbed`); assert((debug.seedLifecycle || []).every((seed) => seed.state !== "absorbed" || !seed.protected), `seed ${seedValue}: protected seeds are not absorbed`); const highMountainSeeds = (seeded.adminCenters || []).filter((p) => { const i = indexOf(p.x, p.y); return seeded.elevation[i] > 0.70 || seeded.slope[i] > 0.52 || seeded.ridgeField[i] > 0.62; }).length; assert(highMountainSeeds <= Math.max(2, Math.ceil((seeded.adminCenters || []).length * 0.12)), `seed ${seedValue}: high mountain admin seeds are rare`); assert(seededAdminMetrics.avgTarget > 0.13, `seed ${seedValue}: municipal borders beat a loose lowland-random barrier baseline`); assert(seededAdminMetrics.denseUrbanRate < 0.52, `seed ${seedValue}: dense urban boundary crossing rate remains low`); assert(seededAdminMetrics.voronoiLikeRate < 0.66, `seed ${seedValue}: Voronoi-like municipal borders do not dominate`); } const deterministicSeedMapsA = [114514, 12345, 54321, 777, 999].map((seedValue) => generateMap(seedValue)); const deterministicSeedMapsB = [114514, 12345, 54321, 777, 999].map((seedValue) => generateMap(seedValue)); for (let n = 0; n < deterministicSeedMapsA.length; n++) { assert(JSON.stringify([...deterministicSeedMapsA[n].adminId]) === JSON.stringify([...deterministicSeedMapsB[n].adminId]), `seed ${[114514, 12345, 54321, 777, 999][n]}: adminId is deterministic`); assert(JSON.stringify(deterministicSeedMapsA[n].adminDebug) === JSON.stringify(deterministicSeedMapsB[n].adminDebug), `seed ${[114514, 12345, 54321, 777, 999][n]}: admin debug metrics are deterministic`); } const byDeposition = capitalNameMaps .map((seeded) => ({ deposition: seeded.terrainTemplate.deposition, lowlandRatio: terrainCoreMetrics(seeded).lowlandRatio })) .sort((a, b) => a.deposition - b.deposition); assert(byDeposition[byDeposition.length - 1].lowlandRatio >= byDeposition[0].lowlandRatio * 0.72, "higher-deposition templates generally preserve or expand lowland area"); CUSTOM_NAME_LIST.push("L1", "L2"); const listedCustomNames = Array.from({ length: 50 }, (_, n) => generateEntityName(9200 + n, `list-probe-${n}`, { x: 10, y: 10, kind: "Probe" }, {}, new Set())); const listedCustomHits = listedCustomNames.filter((name) => name === "L1" || name === "L2").length; assert(NAME_PROBABILITIES.customNameList > 0 && listedCustomHits > 0 && listedCustomHits < listedCustomNames.length, "CUSTOM_NAME_LIST supplies probabilistic selected place names"); CUSTOM_NAME_LIST.length = 0; assert(!validateGeneratedName("青青").valid && validateGeneratedName("青青").reason === "repeatedKanji", "place names reject repeated kanji"); for (const seed of [101, 2026, 54321]) { const seeded = generateMap(seed); const metrics = adminBoundaryMetrics(seeded); const seededRegional = regionalComponentMetrics(seeded); const seededSatellites = satelliteMunicipalityMetrics(seeded); const invalidLandCells = [...seeded.adminId].filter((id, i) => seeded.prefectureMask[i] && !seeded.sea[i] && id < 0).length; const invalidRegionCells = [...seeded.prefectureRegionId].filter((id, i) => !seeded.sea[i] && id < 0).length; assert(invalidLandCells === 0, `seed ${seed}: every prefecture land cell has a valid adminId`); assert(invalidRegionCells === 0, `seed ${seed}: every regional land cell has a valid regionId`); assert(seeded.adminBorders.length > 0, `seed ${seed}: municipal borders exist`); assert(seeded.regionalPrefectureBorders.length > 0, `seed ${seed}: regional prefecture borders exist`); assert(seeded.regionalDebug?.prefecturesGeneratedAfterMunicipalities === true, `seed ${seed}: prefectures are generated after municipalities`); assert(seeded.regionalDebug?.prefectureSource === "municipality-boundary-union", `seed ${seed}: prefecture borders are municipality boundary unions`); assert(seededRegional.maxComponents === 1, `seed ${seed}: every final regional prefecture is connected`); assert(regionalEnclaveCount(seeded) === 0, `seed ${seed}: final regional prefectures have no one-region enclosed enclaves`); assert(regionalBorderMetrics(seeded).invalidSame === 0, `seed ${seed}: regional borders separate final prefecture ids`); const seededHierarchy = borderHierarchyViolations(seeded); assert(seededHierarchy.prefectureCutsMunicipality === 0, `seed ${seed}: prefecture borders do not cut municipalities`); assert(seededHierarchy.municipalityCutsCompartment === 0, `seed ${seed}: municipal borders do not cut natural compartments`); assert(seeded.adminDebug && seeded.adminDebug.compartmentCount > 0, `seed ${seed}: natural compartments are built`); assert(seeded.adminDebug.naturalCompartmentCount > metrics.municipalityCount, `seed ${seed}: compartments are finer than municipalities`); assert(seeded.adminDebug.targetMunicipalityCount >= 20 && seeded.adminDebug.actualMunicipalityCount >= 18, `seed ${seed}: municipality count is dense enough`); assert(seeded.adminDebug.seedCellRevivalCount === 0, `seed ${seed}: seed cell revival stays disabled`); assert(seeded.adminDebug.finalTinyMunicipalityCount <= Math.max(3, Math.ceil(seeded.adminDebug.finalMunicipalityCount * 0.18)), `seed ${seed}: tiny final municipalities are limited`); assert(seeded.adminDebug.changedAfterCompartmentAssignment > 0, `seed ${seed}: compartment assignment is not a no-op`); assert(Array.isArray(seeded.adminDebug.compartmentBorders) && seeded.adminDebug.compartmentBorders.length > seeded.adminBorders.length, `seed ${seed}: compartment border debug exists`); assert(seeded.regionalDebug?.municipalityGraphNodeCount >= metrics.municipalityCount, `seed ${seed}: prefecture graph is based on municipalities`); assert(seeded.adminDebug.changedAfterCompartmentAssignment > 0 || seeded.adminDebug.changedAfterLandscapePartition > 0 || seeded.adminDebug.changedAfterSnap > 0, `seed ${seed}: municipal compartment or terrain passes change cells`); assert(seededSatellites.largeTooSmall.length === 0, `seed ${seed}: large satellites are not tiny independent municipalities`); assert(seededSatellites.independent.length < 3 || seededSatellites.small.length / seededSatellites.independent.length <= 0.35, `seed ${seed}: tiny satellite municipalities remain uncommon`); assert(metrics.municipalityCount >= 18, `seed ${seed}: municipality count remains reasonable`); assert(metrics.centerValidRatio >= 0.90, `seed ${seed}: municipality centers remain valid`); assert(metrics.maxComponents <= 5, `seed ${seed}: topology repair limits disconnected fragments`); assert(metrics.avgTarget > 0.14, `seed ${seed}: borders retain terrain-boundary affinity`); assert(metrics.denseUrbanRate < 0.50, `seed ${seed}: borders avoid excessive dense urban cuts`); assert(metrics.voronoiLikeRate < 0.66, `seed ${seed}: weak-terrain Voronoi-like border ratio stays bounded`); assert(metrics.lowScoreFlatRate < 0.50, `seed ${seed}: low-score flat border ratio stays bounded`); assert(metrics.areaDiversity > 1.25, `seed ${seed}: municipality sizes are not overly uniform`); assert(majorCityCoreIntegrity(seeded) >= 0.55, `seed ${seed}: major city cores remain coherent`); assert(seeded.prefecturalCapital && seeded.adminId[indexOf(seeded.prefecturalCapital.x, seeded.prefecturalCapital.y)] >= 0, `seed ${seed}: capital municipality is not deleted`); } result.className = failed === 0 ? "ok" : "ng"; result.textContent = `${failed === 0 ? "All tests passed." : `${failed} tests failed.`}\n\n${logLines.join("\n")}`; if (!IS_BROWSER) console.log(result.textContent); } catch (error) { result.className = "ng"; result.textContent = String(error?.stack || error); if (!IS_BROWSER) { console.error(result.textContent); process.exitCode = 1; } }