152 lines
7.8 KiB
JavaScript
152 lines
7.8 KiB
JavaScript
|
|
import { generateMap, MAP_W, MAP_H, indexOf } from "./mapGenerator.js";
|
||
|
|
|
||
|
|
const result = document.getElementById("result");
|
||
|
|
const logLines = [];
|
||
|
|
let failed = 0;
|
||
|
|
|
||
|
|
function assert(condition, message) {
|
||
|
|
if (condition) logLines.push(`OK: ${message}`);
|
||
|
|
else {
|
||
|
|
failed += 1;
|
||
|
|
logLines.push(`NG: ${message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
assert(map.elevation.length === size, "elevation length matches map size");
|
||
|
|
assert(map.sea.length === size, "sea length matches 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(Array.isArray(map.bridges) && Array.isArray(map.tunnels) && Array.isArray(map.harborWorks), "legacy bridge/tunnel arrays and harbor arrays exist");
|
||
|
|
assert(map.prefectureRegionId.length === size && Array.isArray(map.regionalPrefectureBorders), "neighbor prefecture regions exist");
|
||
|
|
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.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(map.harborWorks.length <= map.ports.length, "harbor works are attached to ports");
|
||
|
|
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 >= 12, "municipality count is sufficiently large");
|
||
|
|
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(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.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");
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|