?
This commit is contained in:
parent
3ac2c116bd
commit
da9d8ef904
11 changed files with 612 additions and 344 deletions
125
test.js
125
test.js
|
|
@ -284,6 +284,97 @@ function terrainCoreMetrics(map) {
|
|||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -401,6 +492,7 @@ try {
|
|||
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");
|
||||
|
|
@ -430,6 +522,11 @@ try {
|
|||
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(Array.isArray(map.bridges) && Array.isArray(map.tunnels) && Array.isArray(map.harborWorks), "legacy bridge/tunnel arrays and harbor arrays exist");
|
||||
|
|
@ -439,6 +536,8 @@ try {
|
|||
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");
|
||||
|
|
@ -495,8 +594,12 @@ try {
|
|||
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.transportDebug && map.transportDebug.requiredNodeCount > 0, "transport required-node debug exists");
|
||||
assert(transportMetrics.requiredCount > 0 && transportMetrics.reachableCount === transportMetrics.requiredCount, "required transport nodes touch the modern network");
|
||||
assert(transportMetrics.largestRequiredComponent === transportMetrics.requiredCount, "required transport nodes are in one connected modern component");
|
||||
assert(transportMetrics.isolatedExternalGateways === 0 && transportMetrics.isolatedMajorCities === 0, "external gateways and major cities are not isolated");
|
||||
assert(map.minorRoads.length > 0, "minor roads exist");
|
||||
assert(map.adminCenters.length >= 12, "municipality count is sufficiently large");
|
||||
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");
|
||||
|
|
@ -544,7 +647,7 @@ try {
|
|||
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.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(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");
|
||||
|
|
@ -553,6 +656,9 @@ try {
|
|||
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) <= Math.max(6, Math.ceil(map.adminCenters.length * 0.18)), "derived names do not dominate municipality names");
|
||||
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");
|
||||
|
|
@ -581,6 +687,8 @@ 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.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");
|
||||
|
|
@ -599,12 +707,21 @@ try {
|
|||
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.transportDebug?.requiredNodeCount === seededTransport.requiredCount, `seed ${seedValue}: required transport node count is exposed`);
|
||||
assert(seededTransport.reachableCount === seededTransport.requiredCount && seededTransport.largestRequiredComponent === seededTransport.requiredCount, `seed ${seedValue}: required transport nodes are connected`);
|
||||
assert(seededTransport.isolatedExternalGateways === 0 && seededTransport.isolatedMajorCities === 0, `seed ${seedValue}: no gateway or major city is isolated`);
|
||||
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) <= Math.max(6, Math.ceil(seeded.adminCenters.length * 0.18)), `seed ${seedValue}: derived names are bounded`);
|
||||
}
|
||||
const byDeposition = capitalNameMaps
|
||||
.map((seeded) => ({ deposition: seeded.terrainTemplate.deposition, lowlandRatio: terrainCoreMetrics(seeded).lowlandRatio }))
|
||||
|
|
@ -650,7 +767,7 @@ try {
|
|||
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 >= 18 && seeded.adminDebug.actualMunicipalityCount >= 16, `seed ${seed}: municipality count is dense enough`);
|
||||
assert(seeded.adminDebug.targetMunicipalityCount >= 20 && seeded.adminDebug.actualMunicipalityCount >= 18, `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`);
|
||||
|
|
@ -658,7 +775,7 @@ try {
|
|||
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`);
|
||||
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`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue