2026-05-20 13:50:56 +09:00
import { generateMap , MAP _W , MAP _H , indexOf } from "./mapGenerator.js" ;
2026-05-21 02:55:58 +09:00
import {
CUSTOM _NAMES ,
FORCED _NAMES ,
NAME _KANJI _POOLS ,
NAME _PARTS ,
NAME _PROBABILITIES ,
NAME _TEMPLATES ,
NAME _TEMPLATE _WEIGHTS ,
2026-05-21 13:20:19 +09:00
generateEntityName ,
2026-05-21 02:55:58 +09:00
generateTemplateName ,
} from "./names.js" ;
2026-05-20 13:50:56 +09:00
const result = document . getElementById ( "result" ) ;
const logLines = [ ] ;
let failed = 0 ;
2026-05-21 02:55:58 +09:00
const [ namesSource , mapGeneratorSource , testSource ] = await Promise . all ( [
fetch ( "./names.js" ) . then ( ( response ) => response . text ( ) ) ,
fetch ( "./mapGenerator.js" ) . then ( ( response ) => response . text ( ) ) ,
fetch ( "./test.js" ) . then ( ( response ) => response . text ( ) ) ,
] ) ;
2026-05-20 13:50:56 +09:00
function assert ( condition , message ) {
if ( condition ) logLines . push ( ` OK: ${ message } ` ) ;
else {
failed += 1 ;
logLines . push ( ` NG: ${ message } ` ) ;
}
}
2026-05-21 03:13:39 +09:00
function terrainBoundaryTargetForMetrics ( map , i ) {
const lu = map . landuse [ i ] ;
const urbanPenalty = Math . min ( 1 , ( lu === 3 ? 1.45 : lu === 2 ? 1.12 : lu === 4 ? 0.95 : lu === 7 ? 0.90 : lu === 8 ? 0.64 : lu === 5 || lu === 6 ? 0.48 : 0 ) + map . populationDensity [ i ] * 1.35 ) ;
const majorRiver = Math . min ( 1 , Math . max ( map . river [ i ] - 0.32 , 0 ) * 1.9 + Math . max ( map . flowAccum [ i ] - 0.38 , 0 ) * 0.75 ) ;
const minorStream = Math . min ( 1 , map . river [ i ] * 0.34 + map . flowAccum [ i ] * 0.18 ) ;
const ridgeDivide = Math . min ( 1 , map . ridgeField [ i ] * 1.55 + Math . max ( 0 , map . elevation [ i ] - 0.54 ) * map . ridgeField [ i ] * 0.95 ) ;
const slopeBreak = Math . min ( 1 , map . slope [ i ] * 0.58 + Math . max ( 0 , map . slope [ i ] - 0.32 ) * 0.68 ) ;
const highGround = Math . max ( 0 , map . elevation [ i ] - 0.56 ) * 0.22 ;
const valleyFloorPenalty = map . valleyField [ i ] * ( majorRiver > 0.34 ? - 0.10 : - 0.62 ) ;
return Math . max ( 0 , Math . min ( 1 , ridgeDivide + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72 ) ) ;
}
function adminBoundaryMetrics ( map ) {
let borderEdges = 0 ;
let targetSum = 0 ;
let denseUrbanEdges = 0 ;
let rightAngleRuns = 0 ;
let voronoiLikeEdges = 0 ;
let lowScoreFlatEdges = 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 . prefectureMask [ i ] || map . sea [ i ] || map . adminId [ i ] < 0 ) continue ;
for ( const [ dx , dy ] of [ [ 1 , 0 ] , [ 0 , 1 ] ] ) {
const ni = indexOf ( x + dx , y + dy ) ;
if ( ! map . prefectureMask [ ni ] || map . sea [ ni ] || map . adminId [ ni ] < 0 || map . adminId [ ni ] === map . adminId [ i ] ) continue ;
borderEdges ++ ;
const edgeTarget = ( terrainBoundaryTargetForMetrics ( map , i ) + terrainBoundaryTargetForMetrics ( map , ni ) ) * 0.5 ;
targetSum += edgeTarget ;
const urban = Math . max ( map . populationDensity [ i ] , map . populationDensity [ ni ] ) > 0.58 || [ 2 , 3 , 4 , 7 , 8 ] . includes ( map . landuse [ i ] ) || [ 2 , 3 , 4 , 7 , 8 ] . includes ( map . landuse [ ni ] ) ;
if ( urban ) denseUrbanEdges ++ ;
const ca = map . adminCenters [ map . adminId [ i ] ] ;
const cb = map . adminCenters [ map . adminId [ ni ] ] ;
if ( ca && cb ) {
const mx = x + dx * 0.5 ;
const my = y + dy * 0.5 ;
const dA = Math . hypot ( mx - ca . x , my - ca . y ) ;
const dB = Math . hypot ( mx - cb . x , my - cb . y ) ;
if ( Math . abs ( dA - dB ) < 4.2 && edgeTarget < 0.40 ) voronoiLikeEdges ++ ;
}
if ( edgeTarget < 0.16 && Math . max ( map . slope [ i ] , map . slope [ ni ] ) < 0.24 && Math . max ( map . ridgeField [ i ] , map . ridgeField [ ni ] ) < 0.28 && Math . max ( map . river [ i ] , map . river [ ni ] ) < 0.26 ) {
lowScoreFlatEdges ++ ;
}
const sideA = indexOf ( x + ( dy ? 1 : 0 ) , y + ( dx ? 1 : 0 ) ) ;
const sideB = indexOf ( x - ( dy ? 1 : 0 ) , y - ( dx ? 1 : 0 ) ) ;
if ( map . prefectureMask [ sideA ] && map . prefectureMask [ sideB ] && ! map . sea [ sideA ] && ! map . sea [ sideB ] ) {
const turnA = map . adminId [ sideA ] !== map . adminId [ i ] && map . adminId [ sideA ] !== map . adminId [ ni ] ;
const turnB = map . adminId [ sideB ] !== map . adminId [ i ] && map . adminId [ sideB ] !== map . adminId [ ni ] ;
if ( ( turnA || turnB ) && terrainBoundaryTargetForMetrics ( map , i ) < 0.46 ) rightAngleRuns ++ ;
}
}
}
}
const ids = new Set ( [ ... map . adminId ] . filter ( ( id , i ) => id >= 0 && map . prefectureMask [ i ] && ! map . sea [ i ] ) ) ;
const areaById = new Map ( ) ;
for ( let i = 0 ; i < map . adminId . length ; i ++ ) {
if ( map . prefectureMask [ i ] && ! map . sea [ i ] && map . adminId [ i ] >= 0 ) areaById . set ( map . adminId [ i ] , ( areaById . get ( map . adminId [ i ] ) || 0 ) + 1 ) ;
}
const areas = [ ... areaById . values ( ) ] . sort ( ( a , b ) => a - b ) ;
const medianArea = areas . length ? areas [ Math . floor ( areas . length / 2 ) ] : 1 ;
const maxArea = areas . length ? areas [ areas . length - 1 ] : 1 ;
let disconnectedMunicipalities = 0 ;
let maxComponents = 0 ;
const seen = new Uint8Array ( MAP _W * MAP _H ) ;
for ( const id of ids ) {
let comps = 0 ;
seen . fill ( 0 ) ;
for ( let i = 0 ; i < map . adminId . length ; i ++ ) {
if ( seen [ i ] || map . adminId [ i ] !== id || ! map . prefectureMask [ i ] || map . sea [ i ] ) continue ;
comps ++ ;
const queue = [ i ] ;
seen [ 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 ( const [ dx , dy ] of [ [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , 1 ] , [ 0 , - 1 ] ] ) {
const nx = x + dx ;
const ny = y + dy ;
if ( nx < 0 || ny < 0 || nx >= MAP _W || ny >= MAP _H ) continue ;
const ni = indexOf ( nx , ny ) ;
if ( seen [ ni ] || map . adminId [ ni ] !== id || ! map . prefectureMask [ ni ] || map . sea [ ni ] ) continue ;
seen [ ni ] = 1 ;
queue . push ( ni ) ;
}
}
}
if ( comps > 1 ) disconnectedMunicipalities ++ ;
maxComponents = Math . max ( maxComponents , comps ) ;
}
const centerValidCount = map . adminCenters . filter ( ( center ) => {
const i = indexOf ( center . x , center . y ) ;
return map . prefectureMask [ i ] && ! map . sea [ i ] && map . adminId [ i ] >= 0 ;
} ) . length ;
return {
borderEdges ,
avgTarget : borderEdges ? targetSum / borderEdges : 0 ,
denseUrbanRate : borderEdges ? denseUrbanEdges / borderEdges : 0 ,
rightAngleRate : borderEdges ? rightAngleRuns / borderEdges : 0 ,
voronoiLikeRate : borderEdges ? voronoiLikeEdges / borderEdges : 0 ,
lowScoreFlatRate : borderEdges ? lowScoreFlatEdges / borderEdges : 0 ,
areaDiversity : maxArea / Math . max ( 1 , medianArea ) ,
municipalityCount : ids . size ,
disconnectedMunicipalities ,
maxComponents ,
centerValidRatio : map . adminCenters . length ? centerValidCount / map . adminCenters . length : 1 ,
} ;
}
function majorCityCoreIntegrity ( map ) {
const majorCities = map . modernCities . filter ( ( city ) => ( city . population || 0 ) >= 180000 ) ;
if ( majorCities . length === 0 ) return 1 ;
let sum = 0 ;
let checked = 0 ;
for ( const city of majorCities ) {
const counts = new Map ( ) ;
const r = Math . ceil ( Math . max ( 3 , city . coreRadius || 4 ) ) ;
for ( let dy = - r ; dy <= r ; dy ++ ) {
for ( let dx = - r ; dx <= r ; dx ++ ) {
const x = city . x + dx ;
const y = city . y + dy ;
if ( x < 0 || y < 0 || x >= MAP _W || y >= MAP _H || Math . hypot ( dx , dy ) > r ) continue ;
const i = indexOf ( x , y ) ;
if ( ! map . prefectureMask [ i ] || map . sea [ i ] ) continue ;
if ( map . landuse [ i ] !== 3 && map . populationDensity [ i ] < 0.38 ) continue ;
const id = map . adminId [ i ] ;
if ( id >= 0 ) counts . set ( id , ( counts . get ( id ) || 0 ) + 1 ) ;
}
}
const total = [ ... counts . values ( ) ] . reduce ( ( a , b ) => a + b , 0 ) ;
if ( total === 0 ) continue ;
sum += Math . max ( ... counts . values ( ) ) / total ;
checked ++ ;
}
return checked ? sum / checked : 1 ;
}
2026-05-21 13:20:19 +09:00
function satelliteMunicipalityMetrics ( map ) {
const areaById = new Map ( ) ;
for ( let i = 0 ; i < map . adminId . length ; i ++ ) {
if ( map . prefectureMask [ i ] && ! map . sea [ i ] && map . adminId [ i ] >= 0 ) areaById . set ( map . adminId [ i ] , ( areaById . get ( map . adminId [ i ] ) || 0 ) + 1 ) ;
}
const rows = ( map . satelliteCities || [ ] )
. filter ( ( sat ) => map . prefectureMask [ indexOf ( sat . x , sat . y ) ] && ! map . sea [ indexOf ( sat . x , sat . y ) ] )
. map ( ( sat ) => {
const admin = map . adminId [ indexOf ( sat . x , sat . y ) ] ;
return { sat , admin , area : areaById . get ( admin ) || 0 } ;
} ) ;
const independent = rows . filter ( ( row ) => row . sat . municipalityClass === "independentSatelliteMunicipality" ) ;
const small = independent . filter ( ( row ) => row . area < 80 ) ;
const largeTooSmall = rows . filter ( ( row ) => ( row . sat . population || 0 ) >= 60000 && row . sat . municipalityClass === "independentSatelliteMunicipality" && row . area < 120 ) ;
const average = independent . length ? independent . reduce ( ( sum , row ) => sum + row . area , 0 ) / independent . length : 0 ;
return { rows , independent , small , largeTooSmall , average } ;
}
function regionalComponentMetrics ( map ) {
const ids = new Set ( [ ... map . prefectureRegionId ] . filter ( ( id , i ) => id >= 0 && ! map . sea [ i ] ) ) ;
const seen = new Uint8Array ( MAP _W * MAP _H ) ;
let maxComponents = 0 ;
for ( const id of ids ) {
seen . fill ( 0 ) ;
let comps = 0 ;
for ( let i = 0 ; i < map . prefectureRegionId . length ; i ++ ) {
if ( seen [ i ] || map . sea [ i ] || map . prefectureRegionId [ i ] !== id ) continue ;
comps ++ ;
const queue = [ i ] ;
seen [ 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 ( const [ dx , dy ] of [ [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , 1 ] , [ 0 , - 1 ] ] ) {
const nx = x + dx ;
const ny = y + dy ;
if ( nx < 0 || ny < 0 || nx >= MAP _W || ny >= MAP _H ) continue ;
const ni = indexOf ( nx , ny ) ;
if ( seen [ ni ] || map . sea [ ni ] || map . prefectureRegionId [ ni ] !== id ) continue ;
seen [ ni ] = 1 ;
queue . push ( ni ) ;
}
}
}
maxComponents = Math . max ( maxComponents , comps ) ;
}
return { regionCount : ids . size , maxComponents } ;
}
2026-05-20 13:50:56 +09:00
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 ;
}
}
2026-05-20 15:02:37 +09:00
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 ) ] ;
2026-05-20 17:15:09 +09:00
const allNameable = map . entitiesForNames || [ ] ;
const uniqueNames = new Set ( allNameable . map ( ( item ) => item . name ) ) ;
const duplicateNameRatio = allNameable . length ? 1 - uniqueNames . size / allNameable . length : 0 ;
2026-05-21 02:55:58 +09:00
const activePoolChars = new Set ( Object . values ( NAME _KANJI _POOLS ) . flat ( ) . flatMap ( ( part ) => Array . from ( String ( part ) ) ) ) ;
const namedEntityCount = [
... map . villages ,
... map . ports ,
... map . crossings ,
... map . passes ,
... map . markets ,
... map . castles ,
... map . castleTowns ,
... map . modernCities ,
... map . stations ,
... map . industrialZones ,
... map . interchanges ,
... map . logisticsParks ,
... map . satelliteCities ,
... map . newTowns ,
... map . castleRuins ,
... map . externalGateways ,
... map . adminCenters ,
] . filter ( ( item ) => item ? . id && item ? . name ) . length ;
2026-05-20 17:15:09 +09:00
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 ) ;
2026-05-21 13:20:19 +09:00
const satelliteMetrics = satelliteMunicipalityMetrics ( map ) ;
const regionalMetrics = regionalComponentMetrics ( map ) ;
2026-05-20 13:50:56 +09:00
2026-05-21 02:55:58 +09:00
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" ) ;
assert ( NAME _TEMPLATE _WEIGHTS && NAME _TEMPLATE _WEIGHTS . generic ? . modifierTerrain > 0 , "NAME_TEMPLATE_WEIGHTS exists" ) ;
assert ( NAME _PROBABILITIES && NAME _PROBABILITIES . contextCategoryWeights ? . generic , "NAME_PROBABILITIES exists" ) ;
const removedContextModule = "placeName" + "Context.js" ;
assert ( ! namesSource . includes ( removedContextModule ) && ! mapGeneratorSource . includes ( removedContextModule ) && ! testSource . includes ( removedContextModule ) , "removed name-context import is absent" ) ;
assert ( Object . keys ( NAME _KANJI _POOLS ) . every ( ( key ) => Array . isArray ( NAME _KANJI _POOLS [ key ] ) ) , "name category pools are centralized arrays" ) ;
2026-05-21 13:20:19 +09:00
assert ( Object . values ( NAME _KANJI _POOLS ) . every ( ( pool ) => pool . every ( ( part ) => typeof part === "string" && ! part . includes ( "\uFFFD" ) ) ) , "configured name category pools contain valid strings" ) ;
2026-05-21 02:55:58 +09:00
assert ( Object . keys ( NAME _PARTS ) . length === 0 , "legacy NAME_PARTS has no hidden candidates" ) ;
const removedContextSuffixConst = "CONTEXT" + "_SUFFIXES" ;
const removedContextSuffixKey = "context" + "Suffixes" ;
assert ( ! namesSource . includes ( removedContextSuffixConst ) && ! namesSource . includes ( removedContextSuffixKey ) , "hidden context suffix arrays are absent" ) ;
assert ( ! /export\s+const\s+NAME_PROBABILITIES[\s\S]*export\s+const\s+NAME_PROBABILITIES/ . test ( namesSource ) , "NAME_PROBABILITIES has one source" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-20 17:15:09 +09:00
assert ( map . settlementCluster . length === size , "settlement cluster field matches map size" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-21 13:20:19 +09:00
assert ( map . regionalDebug && Number . isFinite ( map . regionalDebug . regionalChangedAfterNaturalPartition ) , "regional changed-cell debug exists" ) ;
assert ( map . regionalDebug . regionalChangedAfterNaturalPartition > 0 , "regional natural partition changes region cells" ) ;
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 ( regionalMetrics . regionCount >= 4 && regionalMetrics . maxComponents <= 5 , "regional prefecture regions remain connected enough for display" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-20 17:15:09 +09:00
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" ) ;
2026-05-21 13:20:19 +09:00
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 . 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" ) ;
assert ( Number . isFinite ( map . adminDebug . satelliteMunicipalitiesCreated ) && Number . isFinite ( map . adminDebug . averageSatelliteMunicipalityArea ) , "satellite municipality debug is available" ) ;
assert ( satelliteMetrics . independent . length < 3 || satelliteMetrics . small . length / satelliteMetrics . independent . length <= 0.35 , "tiny independent satellite municipalities are not the dominant pattern" ) ;
assert ( satelliteMetrics . largeTooSmall . length === 0 , "large independent satellites have meaningful municipal area" ) ;
assert ( satelliteMetrics . independent . length < 3 || satelliteMetrics . average >= 140 , "average independent satellite municipality area is meaningful" ) ;
assert ( satelliteMetrics . rows . every ( ( row ) => row . area >= 80 || row . sat . municipalityClass === "smallTownAttachedToRuralMunicipality" || row . sat . municipalityClass === "suburbanDistrictMergedWithParent" || row . sat . municipalityClass === "newTownDistrict" ) , "tiny satellite areas are merged or explicitly classified as attached districts" ) ;
2026-05-20 17:15:09 +09:00
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" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-20 15:02:37 +09:00
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" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-21 02:55:58 +09:00
assert ( map . entitiesForNames . every ( ( item ) => typeof item . name === "string" ) , "nameable entity names are strings" ) ;
2026-05-20 17:15:09 +09:00
assert ( map . entitiesForNames . every ( ( item ) => ! String ( item . name ) . includes ( "\uFFFD" ) ) , "generated names contain no replacement characters" ) ;
2026-05-21 02:55:58 +09:00
assert ( map . entitiesForNames . every ( ( item ) => Array . from ( String ( item . name ) ) . length >= 2 ) , "one-character generated names are prevented" ) ;
assert ( map . entitiesForNames . every ( ( item ) => String ( item . name ) . length > 0 ) , "empty generated names are prevented" ) ;
2026-05-20 17:15:09 +09:00
assert ( duplicateNameRatio < 0.18 , "generated place-name duplicates stay low" ) ;
2026-05-21 02:55:58 +09:00
assert ( map . nameDebug && Array . isArray ( map . nameDebug . emptyPools ) , "nameDebug reports empty pools" ) ;
2026-05-21 13:20:19 +09:00
assert ( map . nameDebug . emptyPools . length === Object . values ( NAME _KANJI _POOLS ) . filter ( ( pool ) => pool . length === 0 ) . length , "nameDebug empty pools match configured pools" ) ;
2026-05-21 02:55:58 +09:00
assert ( map . nameDebug . selectedTemplateCounts && typeof map . nameDebug . selectedTemplateCounts === "object" , "nameDebug selectedTemplateCounts exists" ) ;
assert ( map . nameDebug . selectedContextCounts && typeof map . nameDebug . selectedContextCounts === "object" , "nameDebug selectedContextCounts exists" ) ;
assert (
map . nameDebug . generatedNamesUsed + map . nameDebug . customNamesUsed + map . nameDebug . forcedNamesUsed + map . nameDebug . fallbackAttempts === namedEntityCount ,
"nameDebug accounting covers named entities"
) ;
2026-05-21 13:20:19 +09:00
assert ( activePoolChars . size > 0 || generateTemplateName ( 777 , "probe-0" , { x : 10 , y : 10 , kind : "Probe" } , { } , 0 , new Set ( ) ) === null , "template generation depends on active pools" ) ;
2026-05-20 17:15:09 +09:00
assert ( villageClusterMean > 0.16 , "villages prefer clustered valley, basin, coastal, and agricultural cells" ) ;
assert ( saneEndpointRatio >= 0.76 , "transport endpoints stay near meaningful generated nodes" ) ;
2026-05-21 02:55:58 +09:00
assert ( Object . keys ( CUSTOM _NAMES ) . length === 0 || NAME _PROBABILITIES . customName < 1 , "CUSTOM_NAMES are probabilistic by default" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-20 17:15:09 +09:00
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" ) ;
2026-05-21 13:20:19 +09:00
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" ) ;
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" ) ;
2026-05-20 17:15:09 +09:00
2026-05-21 02:55:58 +09:00
CUSTOM _NAMES [ "city-0" ] = "C1" ;
const customSameA = generateMap ( 321 ) ;
const customSameB = generateMap ( 321 ) ;
const sameTargetA = customSameA . modernCities . find ( ( item ) => item . id === "city-0" ) ;
const sameTargetB = customSameB . modernCities . find ( ( item ) => item . id === "city-0" ) ;
const customSeedMaps = [ 301 , 302 , 303 , 304 , 305 , 306 , 307 , 308 ] . map ( ( seedValue ) => generateMap ( seedValue ) ) ;
const customTargets = customSeedMaps . map ( ( seeded ) => seeded . modernCities . find ( ( item ) => item . id === "city-0" ) ) . filter ( Boolean ) ;
const customHits = customTargets . filter ( ( item ) => item . name === "C1" ) . length ;
assert ( sameTargetA ? . name === sameTargetB ? . name , "custom-name probability is deterministic for the same seed" ) ;
assert ( ! FORCED _NAMES [ "city-0" ] && customTargets . length > 0 && customHits < customTargets . length , "CUSTOM_NAMES do not force every seed" ) ;
delete CUSTOM _NAMES [ "city-0" ] ;
2026-05-21 13:20:19 +09:00
CUSTOM _NAMES [ "custom-probe" ] = "C1" ;
const directCustomNames = Array . from ( { length : 40 } , ( _ , n ) => generateEntityName ( 9000 + n , "custom-probe" , { x : 10 , y : 10 , kind : "Probe" } , { } , new Set ( ) ) ) ;
const directCustomHits = directCustomNames . filter ( ( name ) => name === "C1" ) . length ;
assert ( NAME _PROBABILITIES . customName > 0 && NAME _PROBABILITIES . customName < 1 && directCustomHits > 0 && directCustomHits < directCustomNames . length , "CUSTOM_NAMES are probabilistic suggestions" ) ;
delete CUSTOM _NAMES [ "custom-probe" ] ;
FORCED _NAMES [ "forced-probe" ] = "F1" ;
assert ( generateEntityName ( 123 , "forced-probe" , { x : 8 , y : 8 , kind : "Probe" } , { } , new Set ( ) , map . nameDebug ) === "F1" , "FORCED_NAMES always apply" ) ;
delete FORCED _NAMES [ "forced-probe" ] ;
2026-05-20 17:15:09 +09:00
for ( const seed of [ 101 , 2026 , 54321 ] ) {
const seeded = generateMap ( seed ) ;
const metrics = adminBoundaryMetrics ( seeded ) ;
2026-05-21 13:20:19 +09:00
const seededRegional = regionalComponentMetrics ( seeded ) ;
const seededSatellites = satelliteMunicipalityMetrics ( seeded ) ;
2026-05-20 17:15:09 +09:00
const invalidLandCells = [ ... seeded . adminId ] . filter ( ( id , i ) => seeded . prefectureMask [ i ] && ! seeded . sea [ i ] && id < 0 ) . length ;
2026-05-21 13:20:19 +09:00
const invalidRegionCells = [ ... seeded . prefectureRegionId ] . filter ( ( id , i ) => ! seeded . sea [ i ] && id < 0 ) . length ;
2026-05-20 17:15:09 +09:00
assert ( invalidLandCells === 0 , ` seed ${ seed } : every prefecture land cell has a valid adminId ` ) ;
2026-05-21 13:20:19 +09:00
assert ( invalidRegionCells === 0 , ` seed ${ seed } : every regional land cell has a valid regionId ` ) ;
2026-05-20 17:15:09 +09:00
assert ( seeded . adminBorders . length > 0 , ` seed ${ seed } : municipal borders exist ` ) ;
2026-05-21 13:20:19 +09:00
assert ( seeded . regionalPrefectureBorders . length > 0 , ` seed ${ seed } : regional prefecture borders exist ` ) ;
assert ( seeded . regionalDebug ? . regionalChangedAfterNaturalPartition > 0 , ` seed ${ seed } : regional natural partition changes cells ` ) ;
assert ( seeded . regionalDebug . regionalNaturalBarrierAverageAfter >= seeded . regionalDebug . regionalNaturalBarrierAverageBefore - 0.10 , ` seed ${ seed } : regional border natural affinity is stable ` ) ;
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 ( 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 ` ) ;
2026-05-20 17:15:09 +09:00
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 ` ) ;
}
2026-05-20 13:50:56 +09:00
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 ) ;
}