tweak
This commit is contained in:
parent
47af930e18
commit
e1bb10ff8a
12 changed files with 1336 additions and 526 deletions
88
test.js
88
test.js
|
|
@ -1,4 +1,6 @@
|
|||
import { generateMap, MAP_W, MAP_H, indexOf } from "./mapGenerator.js";
|
||||
import { CUSTOM_NAMES } from "./names.js";
|
||||
import { adminBoundaryMetrics, majorCityCoreIntegrity } from "./qualityMetrics.js";
|
||||
|
||||
const result = document.getElementById("result");
|
||||
const logLines = [];
|
||||
|
|
@ -74,6 +76,48 @@ try {
|
|||
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 coastalSuffixes = ["\u6d5c", "\u6e4a", "\u6e2f", "\u6d66", "\u6d25", "\u5d0e", "\u6e7e"];
|
||||
const mountainSuffixes = ["\u8c37", "\u5ce0", "\u5c3e\u6839", "\u5c71", "\u6ca2", "\u9e93"];
|
||||
const farInlandCoastalNames = allNameable.filter((p) => {
|
||||
const i = indexOf(p.x, p.y);
|
||||
return !p.portClass && (map.coastalLowland[i] || 0) < 0.12 && coastalSuffixes.some((suffix) => p.name?.endsWith(suffix));
|
||||
}).length;
|
||||
const flatCoastalMountainNames = allNameable.filter((p) => {
|
||||
const i = indexOf(p.x, p.y);
|
||||
return (map.coastalLowland[i] || 0) > 0.35 && (map.slope[i] || 0) < 0.18 && mountainSuffixes.some((suffix) => p.name?.endsWith(suffix));
|
||||
}).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);
|
||||
|
||||
assert(map.elevation.length === size, "elevation length matches map size");
|
||||
assert(map.sea.length === size, "sea length matches map size");
|
||||
|
|
@ -84,6 +128,7 @@ 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.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");
|
||||
assert(Array.isArray(map.tributaryRivers) && Array.isArray(map.smallStreams), "river hierarchy arrays exist");
|
||||
|
|
@ -132,6 +177,17 @@ try {
|
|||
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(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(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");
|
||||
|
|
@ -148,6 +204,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.entitiesForNames.every((item) => !String(item.name).includes("\uFFFD")), "generated names contain no replacement characters");
|
||||
assert(duplicateNameRatio < 0.18, "generated place-name duplicates stay low");
|
||||
assert(farInlandCoastalNames <= Math.max(2, Math.ceil(allNameable.length * 0.12)), "coastal suffixes are not overused far inland");
|
||||
assert(flatCoastalMountainNames <= Math.max(2, Math.ceil(allNameable.length * 0.10)), "mountain suffixes are not overused on flat coastal lowlands");
|
||||
assert(villageClusterMean > 0.16, "villages prefer clustered valley, basin, coastal, and agricultural cells");
|
||||
assert(saneEndpointRatio >= 0.76, "transport endpoints stay near meaningful generated nodes");
|
||||
if (Object.keys(CUSTOM_NAMES).length > 0) {
|
||||
assert(map.entitiesForNames.every((item) => !CUSTOM_NAMES[item.id] || item.name === CUSTOM_NAMES[item.id]), "CUSTOM_NAMES override generated names");
|
||||
} else {
|
||||
assert(true, "CUSTOM_NAMES override hook remains available");
|
||||
}
|
||||
|
||||
assert(
|
||||
map.adminCenters.length !== other.adminCenters.length ||
|
||||
|
|
@ -159,6 +226,27 @@ try {
|
|||
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");
|
||||
|
||||
for (const seed of [101, 2026, 54321]) {
|
||||
const seeded = generateMap(seed);
|
||||
const metrics = adminBoundaryMetrics(seeded);
|
||||
const invalidLandCells = [...seeded.adminId].filter((id, i) => seeded.prefectureMask[i] && !seeded.sea[i] && id < 0).length;
|
||||
assert(invalidLandCells === 0, `seed ${seed}: every prefecture land cell has a valid adminId`);
|
||||
assert(seeded.adminBorders.length > 0, `seed ${seed}: municipal borders exist`);
|
||||
assert(metrics.municipalityCount >= 8, `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")}`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue