805 lines
53 KiB
JavaScript
805 lines
53 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, rendererSource, 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("./renderer.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;
|
|
for (const id of ids) {
|
|
seen.fill(0);
|
|
let comps = 0;
|
|
for (let i = 0; i < map.prefectureRegionId.length; i++) {
|
|
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);
|
|
}
|
|
return { regionCount: ids.size, maxComponents };
|
|
}
|
|
|
|
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.regionalDebug && Number.isFinite(map.regionalDebug.regionalChangedAfterNaturalPartition), "regional changed-cell debug exists");
|
|
assert(map.regionalDebug.regionalChangedAfterNaturalPartition > 0, "regional natural partition changes region cells");
|
|
assert(map.regionalDebug.regionalBorderCountBefore > 0 && map.regionalDebug.regionalBorderCountAfter > 0, "regional border counts are tracked");
|
|
assert(map.regionalDebug.regionalNaturalBarrierAverageAfter >= map.regionalDebug.regionalNaturalBarrierAverageBefore - 0.08, "regional border natural-barrier affinity does not degrade meaningfully");
|
|
assert(map.regionalDebug.regionalVoronoiLikeRateAfter <= map.regionalDebug.regionalVoronoiLikeRateBefore + 0.22, "regional weak Voronoi-like border rate stays bounded");
|
|
assert(map.regionalDebug.compartmentCount > 0 && map.regionalDebug.changedAfterCompartmentAssignment > 0, "regional compartment assignment debug is available");
|
|
assert(Number.isFinite(map.regionalDebug.borderNaturalBarrierAverage) && Number.isFinite(map.regionalDebug.voronoiLikeRate), "regional natural-border aliases are exposed");
|
|
assert(regionalMetrics.regionCount >= 4 && regionalMetrics.maxComponents <= 5, "regional prefecture regions remain connected enough for display");
|
|
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(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 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.prefectureRegionId]) === JSON.stringify([...againB.prefectureRegionId]), "regional prefecture ids 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");
|
|
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?.regionalChangedAfterNaturalPartition > 0, `seed ${seed}: regional natural partition changes cells`);
|
|
assert(seeded.regionalDebug.regionalNaturalBarrierAverageAfter >= seeded.regionalDebug.regionalNaturalBarrierAverageBefore - 0.10, `seed ${seed}: regional border natural affinity is stable`);
|
|
assert(seeded.regionalDebug.regionalVoronoiLikeRateAfter <= seeded.regionalDebug.regionalVoronoiLikeRateBefore + 0.25, `seed ${seed}: regional Voronoi-like rate is bounded`);
|
|
assert(seededRegional.maxComponents <= 5, `seed ${seed}: regional regions remain connected enough`);
|
|
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?.changedAfterCompartmentAssignment > 0, `seed ${seed}: regional compartment assignment changes cells`);
|
|
assert(seeded.regionalDebug?.borderNaturalBarrierAverage > 0.12, `seed ${seed}: regional borders have natural barrier affinity`);
|
|
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);
|
|
}
|