map/test.js
2026-05-26 15:32:27 +09:00

1001 lines
63 KiB
JavaScript

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 result = document.getElementById("result");
const logLines = [];
let failed = 0;
const [namesSource, mapGeneratorSource, mapOutputSource, mapTerrainSource, rendererSource, appSource, mapPipelineSource, mapAdminStageSource, testSource] = await Promise.all([
fetch("./names.js").then((response) => response.text()),
fetch("./mapGenerator.js").then((response) => response.text()),
fetch("./mapOutput.js").then((response) => response.text()),
fetch("./mapTerrain.js").then((response) => response.text()),
fetch("./renderer.js").then((response) => response.text()),
fetch("./app.js").then((response) => response.text()),
fetch("./mapPipeline.js").then((response) => response.text()),
fetch("./mapAdminStage.js").then((response) => response.text()),
fetch("./test.js").then((response) => response.text()),
]);
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(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");
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")}`;
} catch (error) {
result.className = "ng";
result.textContent = String(error?.stack || error);
}