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 = []; 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; } } 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 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"); 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.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"); 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(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"); 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.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 || 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"); 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")}`; } catch (error) { result.className = "ng"; result.textContent = String(error?.stack || error); }