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 {
2026-05-26 00:45:01 +09:00
CUSTOM _NAME _LIST ,
2026-05-21 02:55:58 +09:00
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 ,
2026-05-26 00:45:01 +09:00
validateGeneratedName ,
2026-05-21 02:55:58 +09:00
} from "./names.js" ;
2026-05-20 13:50:56 +09:00
const result = document . getElementById ( "result" ) ;
const logLines = [ ] ;
let failed = 0 ;
2026-05-22 13:57:52 +09:00
const [ namesSource , mapGeneratorSource , mapOutputSource , rendererSource , testSource ] = await Promise . all ( [
2026-05-21 02:55:58 +09:00
fetch ( "./names.js" ) . then ( ( response ) => response . text ( ) ) ,
fetch ( "./mapGenerator.js" ) . then ( ( response ) => response . text ( ) ) ,
2026-05-22 13:57:52 +09:00
fetch ( "./mapOutput.js" ) . then ( ( response ) => response . text ( ) ) ,
fetch ( "./renderer.js" ) . then ( ( response ) => response . text ( ) ) ,
2026-05-21 02:55:58 +09:00
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-21 22:03:14 +09:00
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 ] ) ) ,
} ;
}
2026-05-22 02:11:18 +09:00
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 ,
} ;
}
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-21 22:03:14 +09:00
const terrainMetrics = terrainCoreMetrics ( map ) ;
2026-05-22 02:11:18 +09:00
const transportMetrics = transportConnectivityMetrics ( 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" ) ;
2026-05-21 22:03:14 +09:00
assert ( map . ocean . length === size && map . lake . length === size , "ocean and lake masks match map size" ) ;
2026-05-20 13:50:56 +09:00
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-21 22:03:14 +09:00
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" ) ;
2026-05-22 02:11:18 +09:00
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" ) ;
2026-05-21 22:03:14 +09:00
assert ( [ "east-west" , "north-south" , "diagonal" ] . includes ( map . terrainTemplate . coastAxis ) && map . terrainTemplate . coastSides ? . length === 2 , "paired coast template parameters are exposed" ) ;
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 ( 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" ) ;
2026-05-22 02:11:18 +09:00
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" ) ;
2026-05-21 13:20:19 +09:00
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" ) ;
2026-05-21 22:03:14 +09:00
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" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-21 22:03:14 +09:00
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" ) ;
2026-05-20 13:50:56 +09:00
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" ) ;
2026-05-22 02:11:18 +09:00
assert ( map . adminCenters . length >= 18 , "municipality center 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" ) ;
2026-05-21 22:03:14 +09:00
assert ( map . adminDebug . changedAfterCompartmentAssignment > 0 || map . adminDebug . changedAfterLandscapePartition > 0 || map . adminDebug . changedAfterSnap > 0 , "municipal compartment or terrain passes change admin cells" ) ;
2026-05-21 13:20:19 +09:00
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 22:03:14 +09:00
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" ) ;
2026-05-22 13:57:52 +09:00
assert ( map . adminCenters . every ( ( item ) => item . name && Array . from ( String ( item . name ) ) . length >= 2 ) , "municipal center names are valid labels" ) ;
assert ( map . adminCenters . some ( ( item ) => item . representativeFeatureName ) , "municipal centers keep representative feature metadata when available" ) ;
2026-05-21 22:03:14 +09:00
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" ) ;
2026-05-22 02:11:18 +09:00
assert ( map . adminDebug . targetMunicipalityCount >= 20 && map . adminDebug . targetMunicipalityCount <= 50 && map . adminDebug . actualMunicipalityCount >= 18 , "municipality target and actual counts are dense enough" ) ;
2026-05-21 22:03:14 +09:00
assert ( map . adminDebug . changedAfterCompartmentAssignment > 0 , "municipal compartment assignment is active" ) ;
2026-05-22 13:57:52 +09:00
assert ( map . adminDebug . seedCellRevivalCount === 0 , "seed cell revival is disabled" ) ;
assert ( map . adminDebug . candidateSeedCount >= map . adminDebug . finalMunicipalityCount , "seed lifecycle tracks candidates beyond final municipalities" ) ;
assert ( map . adminDebug . absorbedSeedCount >= 0 && map . adminDebug . pendingSeedCount === 0 , "unresolved pending seeds are absorbed" ) ;
assert ( map . adminDebug . finalTinyMunicipalityCount <= Math . max ( 3 , Math . ceil ( map . adminDebug . finalMunicipalityCount * 0.16 ) ) , "tiny municipalities remain a small fraction" ) ;
2026-05-21 22:03:14 +09:00
assert ( Array . isArray ( map . adminDebug . compartmentBorders ) && map . adminDebug . compartmentBorders . length > map . adminBorders . length , "natural compartment borders are available for debug rendering" ) ;
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" ) ;
2026-05-26 00:45:01 +09:00
assert ( map . entitiesForNames . every ( ( item ) => validateGeneratedName ( item . name , { allowAsciiDiagnostic : true } ) . valid ) , "generated names pass place-name validation" ) ;
2026-05-21 02:55:58 +09:00
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-22 02:11:18 +09:00
assert ( map . nameDebug . oneKanjiAppendFallbackUsed === 0 , "one-kanji append fallback is never used" ) ;
2026-05-22 13:57:52 +09:00
assert ( ( map . nameDebug . derivedNameCount || 0 ) === 0 , "derived municipality suffix names are not generated" ) ;
2026-05-22 02:11:18 +09:00
assert ( ( map . nameDebug . maxDerivedPerBase || 0 ) <= 2 , "derived names per base stay small" ) ;
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 (
2026-05-26 00:45:01 +09:00
map . nameDebug . generatedNamesUsed + map . nameDebug . customNameListUsed + map . nameDebug . fallbackAttempts === namedEntityCount ,
2026-05-21 02:55:58 +09:00
"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-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" ) ;
2026-05-22 02:11:18 +09:00
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" ) ;
2026-05-21 22:03:14 +09:00
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" ) ;
2026-05-21 13:20:19 +09:00
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-21 22:03:14 +09:00
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 ` ) ;
2026-05-22 02:11:18 +09:00
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 ` ) ;
2026-05-21 22:03:14 +09:00
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 ` ) ;
2026-05-22 02:11:18 +09:00
const seededTransport = transportConnectivityMetrics ( seeded ) ;
2026-05-21 22:03:14 +09:00
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 ` ) ;
2026-05-22 02:11:18 +09:00
assert ( seeded . nameDebug ? . oneKanjiAppendFallbackUsed === 0 , ` seed ${ seedValue } : one-kanji append fallback stays unused ` ) ;
2026-05-22 13:57:52 +09:00
assert ( ( seeded . nameDebug ? . derivedNameCount || 0 ) === 0 , ` seed ${ seedValue } : derived suffix names stay disabled ` ) ;
const seededAdminMetrics = adminBoundaryMetrics ( seeded ) ;
const debug = seeded . adminDebug || { } ;
assert ( seededAdminMetrics . municipalityCount >= 18 && seededAdminMetrics . municipalityCount <= 50 , ` seed ${ seedValue } : municipality count stays in target range ` ) ;
assert ( debug . naturalCompartmentCount >= debug . actualMunicipalityCount * 2.5 , ` seed ${ seedValue } : natural compartments are substantially finer than municipalities ` ) ;
assert ( debug . naturalCompartmentCount <= debug . actualMunicipalityCount * 10 , ` seed ${ seedValue } : natural compartments do not become noisy cells ` ) ;
assert ( debug . averageCompartmentsPerMunicipality >= 2.5 , ` seed ${ seedValue } : municipalities group multiple compartments on average ` ) ;
assert ( debug . singleCompartmentMunicipalityRatio < 0.35 , ` seed ${ seedValue } : one-compartment municipalities are uncommon ` ) ;
assert ( debug . seedCellRevivalCount === 0 , ` seed ${ seedValue } : seed cells are not revived after absorption ` ) ;
assert ( debug . finalMunicipalityCount >= 18 && debug . finalMunicipalityCount <= 50 , ` seed ${ seedValue } : final municipality count remains bounded ` ) ;
assert ( debug . finalTinyMunicipalityCount <= Math . max ( 3 , Math . ceil ( debug . finalMunicipalityCount * 0.16 ) ) , ` seed ${ seedValue } : tiny municipalities stay uncommon ` ) ;
assert ( debug . pendingSeedsUsedForLowlandSplit > 0 || debug . absorbedSeedCount > 0 || debug . finalMunicipalityCount >= Math . min ( 20 , debug . targetMunicipalityCount ) , ` seed ${ seedValue } : pending seeds are either used for lowland splits or absorbed ` ) ;
assert ( ( debug . seedLifecycle || [ ] ) . every ( ( seed ) => seed . state !== "absorbed" || ! seed . protected ) , ` seed ${ seedValue } : protected seeds are not absorbed ` ) ;
const highMountainSeeds = ( seeded . adminCenters || [ ] ) . filter ( ( p ) => {
const i = indexOf ( p . x , p . y ) ;
return seeded . elevation [ i ] > 0.70 || seeded . slope [ i ] > 0.52 || seeded . ridgeField [ i ] > 0.62 ;
} ) . length ;
assert ( highMountainSeeds <= Math . max ( 2 , Math . ceil ( ( seeded . adminCenters || [ ] ) . length * 0.12 ) ) , ` seed ${ seedValue } : high mountain admin seeds are rare ` ) ;
assert ( seededAdminMetrics . avgTarget > 0.13 , ` seed ${ seedValue } : municipal borders beat a loose lowland-random barrier baseline ` ) ;
assert ( seededAdminMetrics . denseUrbanRate < 0.52 , ` seed ${ seedValue } : dense urban boundary crossing rate remains low ` ) ;
assert ( seededAdminMetrics . voronoiLikeRate < 0.66 , ` seed ${ seedValue } : Voronoi-like municipal borders do not dominate ` ) ;
}
const deterministicSeedMapsA = [ 114514 , 12345 , 54321 , 777 , 999 ] . map ( ( seedValue ) => generateMap ( seedValue ) ) ;
const deterministicSeedMapsB = [ 114514 , 12345 , 54321 , 777 , 999 ] . map ( ( seedValue ) => generateMap ( seedValue ) ) ;
for ( let n = 0 ; n < deterministicSeedMapsA . length ; n ++ ) {
assert ( JSON . stringify ( [ ... deterministicSeedMapsA [ n ] . adminId ] ) === JSON . stringify ( [ ... deterministicSeedMapsB [ n ] . adminId ] ) , ` seed ${ [ 114514 , 12345 , 54321 , 777 , 999 ] [ n ] } : adminId is deterministic ` ) ;
assert ( JSON . stringify ( deterministicSeedMapsA [ n ] . adminDebug ) === JSON . stringify ( deterministicSeedMapsB [ n ] . adminDebug ) , ` seed ${ [ 114514 , 12345 , 54321 , 777 , 999 ] [ n ] } : admin debug metrics are deterministic ` ) ;
2026-05-21 22:03:14 +09:00
}
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" ) ;
2026-05-26 00:45:01 +09:00
CUSTOM _NAME _LIST . push ( "L1" , "L2" ) ;
const listedCustomNames = Array . from ( { length : 50 } , ( _ , n ) => generateEntityName ( 9200 + n , ` list-probe- ${ n } ` , { x : 10 , y : 10 , kind : "Probe" } , { } , new Set ( ) ) ) ;
const listedCustomHits = listedCustomNames . filter ( ( name ) => name === "L1" || name === "L2" ) . length ;
assert ( NAME _PROBABILITIES . customNameList > 0 && listedCustomHits > 0 && listedCustomHits < listedCustomNames . length , "CUSTOM_NAME_LIST supplies probabilistic selected place names" ) ;
CUSTOM _NAME _LIST . length = 0 ;
2026-05-20 17:15:09 +09:00
2026-05-26 00:45:01 +09:00
assert ( ! validateGeneratedName ( "青青" ) . valid && validateGeneratedName ( "青青" ) . reason === "repeatedKanji" , "place names reject repeated kanji" ) ;
2026-05-21 13:20:19 +09:00
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 ` ) ;
2026-05-21 22:03:14 +09:00
assert ( seeded . adminDebug . naturalCompartmentCount > metrics . municipalityCount , ` seed ${ seed } : compartments are finer than municipalities ` ) ;
2026-05-22 02:11:18 +09:00
assert ( seeded . adminDebug . targetMunicipalityCount >= 20 && seeded . adminDebug . actualMunicipalityCount >= 18 , ` seed ${ seed } : municipality count is dense enough ` ) ;
2026-05-22 13:57:52 +09:00
assert ( seeded . adminDebug . seedCellRevivalCount === 0 , ` seed ${ seed } : seed cell revival stays disabled ` ) ;
assert ( seeded . adminDebug . finalTinyMunicipalityCount <= Math . max ( 3 , Math . ceil ( seeded . adminDebug . finalMunicipalityCount * 0.18 ) ) , ` seed ${ seed } : tiny final municipalities are limited ` ) ;
2026-05-21 22:03:14 +09:00
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 ` ) ;
2026-05-21 13:20:19 +09:00
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-22 02:11:18 +09:00
assert ( metrics . municipalityCount >= 18 , ` seed ${ seed } : municipality count remains reasonable ` ) ;
2026-05-20 17:15:09 +09:00
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 ) ;
}