This commit is contained in:
33333-33333 2026-05-21 22:03:14 +09:00
commit 3fe9c31453
12 changed files with 963 additions and 127 deletions

127
test.js
View file

@ -219,6 +219,71 @@ function regionalComponentMetrics(map) {
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])),
};
}
try {
const map = generateMap(12345);
const other = generateMap(54321);
@ -335,6 +400,7 @@ try {
const cityCoreIntegrity = majorCityCoreIntegrity(map);
const satelliteMetrics = satelliteMunicipalityMetrics(map);
const regionalMetrics = regionalComponentMetrics(map);
const terrainMetrics = terrainCoreMetrics(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");
@ -352,6 +418,7 @@ try {
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");
@ -359,6 +426,11 @@ try {
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(["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(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");
@ -404,10 +476,21 @@ try {
}
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.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");
@ -422,7 +505,7 @@ try {
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.changedAfterLandscapePartition > 0 || map.adminDebug.changedAfterSnap > 0, "municipal terrain partition or snap changes admin cells");
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");
@ -453,6 +536,17 @@ try {
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.filter((item) => item.representativeFeatureName && String(item.name).includes(item.representativeFeatureName)).length >= Math.max(1, Math.floor(map.adminCenters.length * 0.70)), "municipal center names relate to representative feature names");
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 >= 18 && map.adminDebug.actualMunicipalityCount >= 16, "municipality target and actual counts are dense enough");
assert(map.adminDebug.changedAfterCompartmentAssignment > 0, "municipal compartment assignment is active");
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");
@ -487,12 +581,35 @@ try {
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.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.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`);
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`);
}
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_NAMES["city-0"] = "C1";
const customSameA = generateMap(321);
@ -532,7 +649,13 @@ try {
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.changedAfterLandscapePartition > 0 || seeded.adminDebug.changedAfterSnap > 0, `seed ${seed}: municipal terrain passes change cells`);
assert(seeded.adminDebug.naturalCompartmentCount > metrics.municipalityCount, `seed ${seed}: compartments are finer than municipalities`);
assert(seeded.adminDebug.targetMunicipalityCount >= 18 && seeded.adminDebug.actualMunicipalityCount >= 16, `seed ${seed}: municipality count is dense enough`);
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 >= 8, `seed ${seed}: municipality count remains reasonable`);