2026-05-21 13:20:19 +09:00
import { generateEntityName } from "./names.js" ;
import { INF , MAP _H , MAP _W , SIZE , MinHeap , clamp , hash2 , indexOf , inside , nearMapEdge , pickEntities , rand , xyOf } from "./mapUtils.js" ;
export function neighbors8 ( x , y ) {
const out = [ ] ;
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 ( inside ( nx , ny ) ) out . push ( [ nx , ny , Math . hypot ( dx , dy ) ] ) ;
}
}
return out ;
}
export function neighbors4 ( x , y ) {
const out = [ ] ;
for ( const [ dx , dy ] of [ [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , 1 ] , [ 0 , - 1 ] ] ) {
const nx = x + dx ;
const ny = y + dy ;
if ( inside ( nx , ny ) ) out . push ( [ nx , ny , 1 ] ) ;
}
return out ;
}
export function distanceToNearest ( points , x , y , fallback = 999 ) {
let best = fallback ;
for ( const p of points ) best = Math . min ( best , Math . hypot ( p . x - x , p . y - y ) ) ;
return best ;
}
2026-05-28 15:48:42 +09:00
export function createPointSpatialIndex ( points , cellSize = 12 ) {
const buckets = new Map ( ) ;
const normalized = ( points || [ ] )
. filter ( ( p ) => p && Number . isFinite ( p . x ) && Number . isFinite ( p . y ) )
. map ( ( p ) => ( { ... p , x : Math . round ( p . x ) , y : Math . round ( p . y ) } ) ) ;
const keyOf = ( cx , cy ) => ` ${ cx } , ${ cy } ` ;
for ( const p of normalized ) {
const cx = Math . floor ( p . x / cellSize ) ;
const cy = Math . floor ( p . y / cellSize ) ;
const key = keyOf ( cx , cy ) ;
let bucket = buckets . get ( key ) ;
if ( ! bucket ) {
bucket = [ ] ;
buckets . set ( key , bucket ) ;
}
bucket . push ( p ) ;
}
function nearestDistanceSq ( x , y , maxDistance = Math . max ( MAP _W , MAP _H ) ) {
if ( ! normalized . length ) return maxDistance * maxDistance ;
const cx = Math . floor ( x / cellSize ) ;
const cy = Math . floor ( y / cellSize ) ;
const maxRing = Number . isFinite ( maxDistance ) ? Math . ceil ( maxDistance / cellSize ) : Math . ceil ( Math . max ( MAP _W , MAP _H ) / cellSize ) ;
let best = maxDistance * maxDistance ;
for ( let ring = 0 ; ring <= maxRing ; ring ++ ) {
for ( let by = cy - ring ; by <= cy + ring ; by ++ ) {
for ( let bx = cx - ring ; bx <= cx + ring ; bx ++ ) {
if ( ring > 0 && bx > cx - ring && bx < cx + ring && by > cy - ring && by < cy + ring ) continue ;
const bucket = buckets . get ( keyOf ( bx , by ) ) ;
if ( ! bucket ) continue ;
for ( const p of bucket ) {
const dx = p . x - x ;
const dy = p . y - y ;
const d2 = dx * dx + dy * dy ;
if ( d2 < best ) best = d2 ;
}
}
}
}
return best ;
}
return {
points : normalized ,
hasWithin ( x , y , radius ) {
return nearestDistanceSq ( x , y , radius ) < radius * radius ;
} ,
distance ( x , y , fallback = 999 ) {
const d2 = nearestDistanceSq ( x , y , fallback ) ;
return d2 < fallback * fallback ? Math . sqrt ( d2 ) : fallback ;
} ,
nearestDistanceSq ,
} ;
}
2026-05-21 13:20:19 +09:00
export function aStar ( start , goal , costAt ) {
const startIndex = indexOf ( start . x , start . y ) ;
const goalIndex = indexOf ( goal . x , goal . y ) ;
if ( startIndex === goalIndex ) return [ [ start . x , start . y ] ] ;
const score = new Float32Array ( SIZE ) ;
const cameFrom = new Int32Array ( SIZE ) ;
const closed = new Uint8Array ( SIZE ) ;
score . fill ( INF ) ;
cameFrom . fill ( - 1 ) ;
const heap = new MinHeap ( ) ;
score [ startIndex ] = 0 ;
heap . push ( { i : startIndex , f : Math . hypot ( start . x - goal . x , start . y - goal . y ) } ) ;
let guard = 0 ;
while ( heap . length > 0 && guard ++ < SIZE * 3 ) {
const current = heap . pop ( ) ;
if ( ! current || closed [ current . i ] ) continue ;
closed [ current . i ] = 1 ;
if ( current . i === goalIndex ) {
const path = [ ] ;
let p = goalIndex ;
while ( p !== - 1 ) {
const [ x , y ] = xyOf ( p ) ;
path . push ( [ x , y ] ) ;
if ( p === startIndex ) break ;
p = cameFrom [ p ] ;
}
return path . reverse ( ) ;
}
const [ cx , cy ] = xyOf ( current . i ) ;
for ( const [ nx , ny , stepDistance ] of neighbors8 ( cx , cy ) ) {
const nextIndex = indexOf ( nx , ny ) ;
if ( closed [ nextIndex ] ) continue ;
const cost = costAt ( nx , ny , cx , cy ) ;
if ( cost >= INF ) continue ;
const nextScore = score [ current . i ] + cost * stepDistance ;
if ( nextScore < score [ nextIndex ] ) {
score [ nextIndex ] = nextScore ;
cameFrom [ nextIndex ] = current . i ;
heap . push ( { i : nextIndex , f : nextScore + Math . hypot ( nx - goal . x , ny - goal . y ) * 0.78 } ) ;
}
}
}
return [ ] ;
}
export function influenceFromPaths ( paths , radius ) {
const grid = new Float32Array ( SIZE ) ;
2026-05-28 15:48:42 +09:00
const r = Math . ceil ( radius ) ;
const r2 = radius * radius ;
2026-05-21 13:20:19 +09:00
for ( const path of paths ) {
for ( const [ x , y ] of path ) {
2026-05-28 15:48:42 +09:00
for ( let dy = - r ; dy <= r ; dy ++ ) {
for ( let dx = - r ; dx <= r ; dx ++ ) {
2026-05-21 13:20:19 +09:00
const nx = x + dx ;
const ny = y + dy ;
if ( ! inside ( nx , ny ) ) continue ;
2026-05-28 15:48:42 +09:00
const d2 = dx * dx + dy * dy ;
if ( d2 > r2 ) continue ;
const d = Math . sqrt ( d2 ) ;
2026-05-21 13:20:19 +09:00
const i = indexOf ( nx , ny ) ;
grid [ i ] = Math . max ( grid [ i ] , 1 / ( 1 + d ) ) ;
}
}
}
}
return grid ;
}
export function pointKey ( p ) {
return ` ${ p . x } , ${ p . y } ` ;
}
export function getDegree ( degreeMap , p ) {
return degreeMap . get ( pointKey ( p ) ) || 0 ;
}
export function incrementDegree ( degreeMap , p ) {
degreeMap . set ( pointKey ( p ) , getDegree ( degreeMap , p ) + 1 ) ;
}
export function nearestConnectable ( points , target , degreeMap , maxDegree = 3 ) {
if ( ! points . length ) return null ;
const sorted = points
. map ( ( p ) => ( { ... p , d : Math . hypot ( p . x - target . x , p . y - target . y ) , degree : getDegree ( degreeMap , p ) } ) )
. sort ( ( a , b ) => ( a . degree >= maxDegree ? 22 : 0 ) + a . d + a . degree * 7 - ( ( b . degree >= maxDegree ? 22 : 0 ) + b . d + b . degree * 7 ) ) ;
return sorted . find ( ( p ) => p . degree < maxDegree ) || sorted [ 0 ] ;
}
export function corridorPenalty ( grid , x , y , hubs , endpoints , strength = 6 ) {
if ( ! grid ) return 0 ;
const value = grid [ indexOf ( x , y ) ] ;
if ( value <= 0.0001 ) return 0 ;
const nearEndpoint = distanceToNearest ( endpoints , x , y ) <= 3.2 ;
if ( nearEndpoint ) return 0 ;
const hubDistance = distanceToNearest ( hubs , x , y ) ;
if ( hubDistance <= 3.5 ) return 0 ;
if ( hubDistance <= 7.5 ) return value * strength * 0.28 ;
return value * strength ;
}
export function nodeAvoidPenalty ( points , x , y , endpoints , radius = 3.0 , strength = 5.0 ) {
if ( ! points || points . length === 0 ) return 0 ;
if ( distanceToNearest ( endpoints , x , y ) <= radius + 0.4 ) return 0 ;
const d = distanceToNearest ( points , x , y ) ;
if ( d >= radius ) return 0 ;
return ( radius - d ) * strength ;
}
export function makeTransportCost ( baseCost , existingPaths , hubs , endpoints , radius = 4 , strength = 6 , avoidPoints = [ ] , avoidRadius = 3.0 , avoidStrength = 5.0 ) {
const grid = existingPaths . length ? influenceFromPaths ( existingPaths , radius ) : null ;
return ( x , y , cx , cy ) => {
const base = baseCost ( x , y , cx , cy ) ;
if ( base >= INF ) return base ;
return base
+ corridorPenalty ( grid , x , y , hubs , endpoints , strength )
+ nodeAvoidPenalty ( avoidPoints , x , y , endpoints , avoidRadius , avoidStrength ) ;
} ;
}
export function pathLength ( path ) {
let total = 0 ;
for ( let i = 1 ; i < path . length ; i ++ ) total += Math . hypot ( path [ i ] [ 0 ] - path [ i - 1 ] [ 0 ] , path [ i ] [ 1 ] - path [ i - 1 ] [ 1 ] ) ;
return total ;
}
export function pathEndpointDistance ( path ) {
if ( ! path || path . length < 2 ) return 0 ;
const a = path [ 0 ] ;
const b = path [ path . length - 1 ] ;
return Math . hypot ( a [ 0 ] - b [ 0 ] , a [ 1 ] - b [ 1 ] ) ;
}
export function pathCompactness ( path ) {
const direct = pathEndpointDistance ( path ) ;
if ( direct <= 0.001 ) return INF ;
return pathLength ( path ) / direct ;
}
export function pathOverlapRatio ( path , existingPaths , radius = 2 ) {
if ( ! path ? . length || ! existingPaths ? . length ) return 0 ;
const grid = influenceFromPaths ( existingPaths , radius ) ;
let overlap = 0 ;
for ( const [ x , y ] of path ) if ( grid [ indexOf ( x , y ) ] > 0.18 ) overlap ++ ;
return overlap / Math . max ( 1 , path . length ) ;
}
export function compactPathArray ( paths , { minLength = 8 , maxOverlap = 0.35 , maxCount = 99 } = { } ) {
const kept = [ ] ;
for ( const path of paths . slice ( ) . sort ( ( a , b ) => pathLength ( b ) - pathLength ( a ) ) ) {
if ( pathLength ( path ) < minLength ) continue ;
if ( pathOverlapRatio ( path , kept , 2 ) > maxOverlap ) continue ;
kept . push ( path ) ;
if ( kept . length >= maxCount ) break ;
}
paths . splice ( 0 , paths . length , ... kept ) ;
}
export function bresenhamCells ( a , b ) {
const cells = [ ] ;
let x0 = a [ 0 ] ;
let y0 = a [ 1 ] ;
const x1 = b [ 0 ] ;
const y1 = b [ 1 ] ;
const dx = Math . abs ( x1 - x0 ) ;
const dy = Math . abs ( y1 - y0 ) ;
const sx = x0 < x1 ? 1 : - 1 ;
const sy = y0 < y1 ? 1 : - 1 ;
let err = dx - dy ;
while ( true ) {
cells . push ( [ x0 , y0 ] ) ;
if ( x0 === x1 && y0 === y1 ) break ;
const e2 = 2 * err ;
if ( e2 > - dy ) { err -= dy ; x0 += sx ; }
if ( e2 < dx ) { err += dx ; y0 += sy ; }
}
return cells ;
}
export function smoothPathByLineOfSight ( path , passable , maxSegment = 9 ) {
if ( ! path || path . length < 3 ) return path || [ ] ;
const out = [ path [ 0 ] ] ;
let i = 0 ;
while ( i < path . length - 1 ) {
let best = i + 1 ;
const limit = Math . min ( path . length - 1 , i + maxSegment ) ;
for ( let j = limit ; j > i + 1 ; j -- ) {
const cells = bresenhamCells ( path [ i ] , path [ j ] ) ;
if ( cells . every ( ( [ x , y ] ) => inside ( x , y ) && passable ( x , y ) ) ) { best = j ; break ; }
}
for ( const cell of bresenhamCells ( path [ i ] , path [ best ] ) . slice ( 1 ) ) out . push ( cell ) ;
i = best ;
}
return out ;
}
export function averagePathField ( path , field ) {
if ( ! path ? . length ) return 0 ;
let sum = 0 ;
for ( const [ x , y ] of path ) sum += field [ indexOf ( x , y ) ] || 0 ;
return sum / path . length ;
}
export function influenceFromPoints ( points , radius , weightFn = ( ) => 1 ) {
const grid = new Float32Array ( SIZE ) ;
2026-05-28 15:48:42 +09:00
const r = Math . ceil ( radius ) ;
const r2 = radius * radius ;
2026-05-21 13:20:19 +09:00
for ( const p of points ) {
const weight = weightFn ( p ) ;
2026-05-28 15:48:42 +09:00
for ( let dy = - r ; dy <= r ; dy ++ ) {
for ( let dx = - r ; dx <= r ; dx ++ ) {
2026-05-21 13:20:19 +09:00
const nx = p . x + dx ;
const ny = p . y + dy ;
if ( ! inside ( nx , ny ) ) continue ;
2026-05-28 15:48:42 +09:00
const d2 = dx * dx + dy * dy ;
if ( d2 > r2 ) continue ;
const d = Math . sqrt ( d2 ) ;
2026-05-21 13:20:19 +09:00
const i = indexOf ( nx , ny ) ;
grid [ i ] = Math . max ( grid [ i ] , weight / ( 1 + d ) ) ;
}
}
}
return grid ;
}
export function samplePath ( path , step ) {
const out = [ ] ;
for ( let i = step ; i < path . length - step ; i += step ) {
const [ x , y ] = path [ i ] ;
out . push ( { x , y , score : 1 } ) ;
}
return out ;
}
export function smoothMask ( mask , passes = 2 ) {
let current = new Uint8Array ( mask ) ;
for ( let pass = 0 ; pass < passes ; pass ++ ) {
const next = new Uint8Array ( current ) ;
for ( let y = 1 ; y < MAP _H - 1 ; y ++ ) {
for ( let x = 1 ; x < MAP _W - 1 ; x ++ ) {
const i = indexOf ( x , y ) ;
let count = 0 ;
for ( let dy = - 1 ; dy <= 1 ; dy ++ ) {
for ( let dx = - 1 ; dx <= 1 ; dx ++ ) {
if ( current [ indexOf ( x + dx , y + dy ) ] ) count ++ ;
}
}
if ( count >= 5 ) next [ i ] = 1 ;
else if ( count <= 3 ) next [ i ] = 0 ;
}
}
current = next ;
}
return current ;
}
export function largestConnectedMask ( mask ) {
const seen = new Uint8Array ( SIZE ) ;
let best = [ ] ;
const queue = [ ] ;
for ( let i = 0 ; i < SIZE ; i ++ ) {
if ( ! mask [ i ] || seen [ i ] ) continue ;
const component = [ ] ;
queue . length = 0 ;
queue . push ( i ) ;
seen [ i ] = 1 ;
for ( let q = 0 ; q < queue . length ; q ++ ) {
const cur = queue [ q ] ;
component . push ( cur ) ;
const [ x , y ] = xyOf ( cur ) ;
for ( const [ nx , ny ] of neighbors8 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( ! mask [ ni ] || seen [ ni ] ) continue ;
seen [ ni ] = 1 ;
queue . push ( ni ) ;
}
}
if ( component . length > best . length ) best = component ;
}
const out = new Uint8Array ( SIZE ) ;
for ( const i of best ) out [ i ] = 1 ;
return out ;
}
export function componentCount ( mask ) {
const seen = new Uint8Array ( SIZE ) ;
const queue = [ ] ;
let count = 0 ;
for ( let i = 0 ; i < SIZE ; i ++ ) {
if ( ! mask [ i ] || seen [ i ] ) continue ;
count ++ ;
queue . length = 0 ;
queue . push ( i ) ;
seen [ i ] = 1 ;
for ( let q = 0 ; q < queue . length ; q ++ ) {
const [ x , y ] = xyOf ( queue [ q ] ) ;
for ( const [ nx , ny ] of neighbors8 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( ! mask [ ni ] || seen [ ni ] ) continue ;
seen [ ni ] = 1 ;
queue . push ( ni ) ;
}
}
}
return count ;
}
export function makePrefectureMask ( seed , sea , elevation , slope , river ) {
const candidates = [ ] ;
for ( let y = 8 ; y < MAP _H - 8 ; y ++ ) {
for ( let x = 8 ; x < MAP _W - 8 ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] ) continue ;
const centrality = 1 - Math . hypot ( ( x / MAP _W ) - 0.5 , ( y / MAP _H ) - 0.5 ) / 0.72 ;
const score = centrality * 0.28 + ( 1 - slope [ i ] ) * 0.42 + ( 1 - Math . abs ( elevation [ i ] - 0.42 ) ) * 0.22 + Math . min ( 0.16 , river [ i ] * 0.08 ) ;
candidates . push ( { x , y , score } ) ;
}
}
const regionSeeds = pickEntities ( candidates , {
max : 1 ,
minDistance : 18 ,
threshold : 0.35 ,
seed : seed + 904 ,
jitter : 0.02 ,
} ) ;
const mask = new Uint8Array ( SIZE ) ;
const dist = new Float32Array ( SIZE ) ;
dist . fill ( INF ) ;
const heap = new MinHeap ( ) ;
const landCells = sea . reduce ( ( a , v ) => a + ( v ? 0 : 1 ) , 0 ) ;
const target = Math . floor ( landCells * ( 0.23 + rand ( seed , 906 ) * 0.08 ) ) ;
for ( const s of regionSeeds ) {
const i = indexOf ( s . x , s . y ) ;
dist [ i ] = 0 ;
heap . push ( { i , f : 0 } ) ;
}
let claimed = 0 ;
while ( heap . length > 0 && claimed < target ) {
const current = heap . pop ( ) ;
if ( ! current ) continue ;
const ci = current . i ;
if ( current . f > dist [ ci ] + 1e-5 || mask [ ci ] ) continue ;
const [ cx , cy ] = xyOf ( ci ) ;
if ( sea [ ci ] ) continue ;
mask [ ci ] = 1 ;
claimed ++ ;
for ( const [ nx , ny , step ] of neighbors8 ( cx , cy ) ) {
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] || mask [ ni ] ) continue ;
const edgePenalty = nearMapEdge ( nx , ny , 2 ) ? 4.2 : nearMapEdge ( nx , ny , 5 ) ? 1.8 : 0 ;
const ridgePenalty = Math . max ( 0 , elevation [ ni ] - 0.5 ) * 5.4 + Math . max ( 0 , elevation [ ni ] - elevation [ ci ] ) * 3.2 ;
const slopePenalty = slope [ ni ] * 4.1 ;
const riverPenalty = river [ ni ] > 0.65 ? 2.2 : river [ ni ] > 0.32 ? 0.9 : 0 ;
const cost = Math . max ( 0.18 , 1 + edgePenalty + ridgePenalty + slopePenalty + riverPenalty + Math . abs ( elevation [ ni ] - elevation [ ci ] ) * 4.2 ) * step ;
const nd = dist [ ci ] + cost ;
if ( nd < dist [ ni ] ) {
dist [ ni ] = nd ;
heap . push ( { i : ni , f : nd } ) ;
}
}
}
return largestConnectedMask ( smoothMask ( mask , 2 ) ) ;
}
export function generateRegionalPrefectures ( seed , sea , elevation , slope , river , ridgeField , flowAccum , anchorMask ) {
const seeded = generateRegionalPrefecturesSeedGrowth ( seed , sea , elevation , slope , river , ridgeField , flowAccum , anchorMask ) ;
const beforeRegionId = new Int16Array ( seeded . regionId ) ;
const naturalBarrierScore = buildRegionalNaturalBarrierScore ( sea , elevation , slope , river , ridgeField , flowAccum ) ;
const beforeBorderCount = countRegionBorderEdges ( beforeRegionId , sea ) ;
const beforeNaturalAverage = averageRegionBorderBarrier ( beforeRegionId , sea , naturalBarrierScore ) ;
const beforeVoronoiLikeRate = regionalVoronoiLikeRate ( beforeRegionId , seeded . centers , sea , naturalBarrierScore ) ;
const { compartmentId , compartments } = buildRegionalNaturalCompartments ( sea , elevation , slope , river , ridgeField , flowAccum , naturalBarrierScore ) ;
const owner = new Int16Array ( compartments . length ) ;
owner . fill ( - 1 ) ;
2026-05-21 22:03:14 +09:00
for ( let id = 0 ; id < seeded . centers . length ; id ++ ) {
const center = seeded . centers [ id ] ;
if ( ! center || ! inside ( center . x , center . y ) ) continue ;
const ci = compartmentId [ indexOf ( center . x , center . y ) ] ;
if ( ci >= 0 ) owner [ ci ] = id ;
}
2026-05-21 13:20:19 +09:00
for ( const unit of compartments ) {
if ( ! unit || unit . area === 0 ) continue ;
2026-05-21 22:03:14 +09:00
if ( unit . cells . some ( ( i ) => anchorMask [ i ] ) ) owner [ unit . id ] = 0 ;
}
for ( let pass = 0 ; pass < compartments . length + 6 ; pass ++ ) {
let changedThisPass = 0 ;
for ( const unit of compartments ) {
if ( ! unit || unit . area === 0 || owner [ unit . id ] >= 0 ) continue ;
let bestOwner = - 1 ;
let bestScore = - INF ;
for ( const [ neighborId , edge ] of unit . adjacent || [ ] ) {
const neighborOwner = owner [ neighborId ] ;
if ( neighborOwner < 0 ) continue ;
const neighbor = compartments [ neighborId ] ;
const center = seeded . centers [ neighborOwner ] ;
const barrier = edge . target / Math . max ( 1 , edge . count ) ;
const sameClass = neighbor ? . classId === unit . classId ? 1.0 : 0 ;
const d = center ? Math . hypot ( unit . x - center . x , unit . y - center . y ) : 0 ;
const score = edge . count * 0.45 + sameClass + neighbor . coastalExposure * 0.08 + neighbor . ridgeExposure * 0.05 - barrier * 2.6 - d * 0.008 ;
if ( score > bestScore ) { bestScore = score ; bestOwner = neighborOwner ; }
}
if ( bestOwner >= 0 ) {
owner [ unit . id ] = bestOwner ;
changedThisPass ++ ;
}
2026-05-21 13:20:19 +09:00
}
2026-05-21 22:03:14 +09:00
if ( changedThisPass === 0 ) break ;
}
for ( const unit of compartments ) {
if ( ! unit || unit . area === 0 || owner [ unit . id ] >= 0 ) continue ;
let bestId = 0 ;
let best = - INF ;
for ( let id = 0 ; id < seeded . centers . length ; id ++ ) {
2026-05-21 13:20:19 +09:00
const center = seeded . centers [ id ] ;
2026-05-21 22:03:14 +09:00
if ( ! center ) continue ;
const d = Math . hypot ( unit . x - center . x , unit . y - center . y ) ;
const score = - d + ( id === 0 ? ( unit . cells . some ( ( i ) => anchorMask [ i ] ) ? 1000 : - 12 ) : 0 ) ;
2026-05-21 13:20:19 +09:00
if ( score > best ) { best = score ; bestId = id ; }
}
2026-05-21 22:03:14 +09:00
owner [ unit . id ] = bestId ;
2026-05-21 13:20:19 +09:00
}
const regionId = new Int16Array ( beforeRegionId ) ;
for ( const unit of compartments ) {
const id = owner [ unit . id ] ;
if ( id < 0 ) continue ;
for ( const i of unit . cells ) regionId [ i ] = anchorMask [ i ] ? 0 : id ;
}
for ( let i = 0 ; i < SIZE ; i ++ ) if ( anchorMask [ i ] && ! sea [ i ] ) regionId [ i ] = 0 ;
for ( let pass = 0 ; pass < 4 ; pass ++ ) repairRegionalTopology ( regionId , sea , seeded . centers , anchorMask , 260 ) ;
2026-05-26 15:32:27 +09:00
repairDisconnectedRegionalPrefectures ( regionId , sea , seeded . centers , anchorMask ) ;
mergeTinyRegionalPrefectures ( regionId , sea , seeded . centers , anchorMask , 720 ) ;
rebalanceOversizedRegionalPrefectures ( regionId , sea , seeded . centers , anchorMask , naturalBarrierScore ) ;
snapRegionalBoundariesToNaturalFeatures ( regionId , sea , anchorMask , naturalBarrierScore , 2 ) ;
repairFinalRegionalTopology ( regionId , sea , seeded . centers , anchorMask , naturalBarrierScore ) ;
2026-05-24 17:38:51 +09:00
2026-05-21 13:20:19 +09:00
let changed = 0 ;
for ( let i = 0 ; i < SIZE ; i ++ ) if ( ! sea [ i ] && beforeRegionId [ i ] !== regionId [ i ] ) changed ++ ;
const afterBorderCount = countRegionBorderEdges ( regionId , sea ) ;
const measuredAfterNaturalAverage = averageRegionBorderBarrier ( regionId , sea , naturalBarrierScore ) ;
2026-05-26 15:32:27 +09:00
const afterNaturalAverage = measuredAfterNaturalAverage ;
2026-05-21 13:20:19 +09:00
const afterVoronoiLikeRate = regionalVoronoiLikeRate ( regionId , seeded . centers , sea , naturalBarrierScore ) ;
return {
regionId ,
centers : seeded . centers ,
naturalBarrierScore ,
debug : {
regionalChangedAfterNaturalPartition : changed ,
regionalBorderCountBefore : beforeBorderCount ,
regionalBorderCountAfter : afterBorderCount ,
regionalVoronoiLikeRateBefore : beforeVoronoiLikeRate ,
regionalVoronoiLikeRateAfter : afterVoronoiLikeRate ,
regionalNaturalBarrierAverageBefore : beforeNaturalAverage ,
regionalNaturalBarrierAverageAfter : afterNaturalAverage ,
2026-05-26 15:32:27 +09:00
regionalDisplayBorderCount : countRegionBorderEdges ( regionId , sea ) ,
regionalDisplayNaturalBarrierAverage : averageRegionBorderBarrier ( regionId , sea , naturalBarrierScore ) ,
2026-05-21 13:20:19 +09:00
regionalCompartmentCount : compartments . filter ( ( unit ) => unit . area > 0 ) . length ,
2026-05-21 22:03:14 +09:00
compartmentCount : compartments . filter ( ( unit ) => unit . area > 0 ) . length ,
changedAfterCompartmentAssignment : changed ,
borderNaturalBarrierAverage : afterNaturalAverage ,
2026-05-22 02:11:18 +09:00
voronoiLikeRate : afterVoronoiLikeRate ,
2026-05-26 15:32:27 +09:00
finalRegionConnectivityMaxComponents : Math . max ( 0 , ... [ ... new Set ( [ ... regionId ] . filter ( ( id , i ) => id >= 0 && ! sea [ i ] ) ) ] . map ( ( id ) => collectRegionComponents ( regionId , sea , id ) . length ) ) ,
finalRegionalEnclaveCount : countRegionalEnclaves ( regionId , sea ) ,
finalRegionalMaxAreaShare : maxRegionalAreaShare ( regionId , sea ) ,
2026-05-21 13:20:19 +09:00
} ,
} ;
}
function generateRegionalPrefecturesSeedGrowth ( seed , sea , elevation , slope , river , ridgeField , flowAccum , anchorMask ) {
const centers = [ ] ;
let sx = 0 ;
let sy = 0 ;
let sc = 0 ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( anchorMask [ i ] ) { sx += x ; sy += y ; sc ++ ; }
}
}
if ( sc > 0 ) centers . push ( { x : Math . round ( sx / sc ) , y : Math . round ( sy / sc ) , score : 2 , kind : "Current Prefecture" } ) ;
const candidates = [ ] ;
const ax = centers [ 0 ] ? . x ? ? MAP _W / 2 ;
const ay = centers [ 0 ] ? . y ? ? MAP _H / 2 ;
for ( let y = 4 ; y < MAP _H - 4 ; y ++ ) {
for ( let x = 4 ; x < MAP _W - 4 ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] || anchorMask [ i ] ) continue ;
const edgePull = Math . max ( Math . abs ( x / MAP _W - 0.5 ) , Math . abs ( y / MAP _H - 0.5 ) ) ;
const awayFromCurrent = Math . hypot ( x - ax , y - ay ) / Math . hypot ( MAP _W , MAP _H ) ;
const settleable = ( 1 - slope [ i ] ) * 0.24 + Math . max ( 0 , 0.62 - elevation [ i ] ) * 0.28 + flowAccum [ i ] * 0.08 ;
const score = edgePull * 0.55 + awayFromCurrent * 0.38 + settleable + hash2 ( x , y , seed + 6100 ) * 0.06 ;
candidates . push ( { x , y , score , kind : "Neighbor Prefecture" } ) ;
}
}
centers . push ( ... pickEntities ( candidates , {
2026-05-26 15:32:27 +09:00
max : 6 + Math . floor ( rand ( seed , 6101 ) * 4 ) ,
minDistance : 31 ,
threshold : 0.42 ,
2026-05-21 13:20:19 +09:00
seed : seed + 6102 ,
jitter : 0.02 ,
} ) ) ;
const regionId = new Int16Array ( SIZE ) ;
regionId . fill ( - 1 ) ;
const dist = new Float32Array ( SIZE ) ;
dist . fill ( INF ) ;
const heap = new MinHeap ( ) ;
centers . forEach ( ( center , id ) => {
const i = indexOf ( center . x , center . y ) ;
if ( sea [ i ] ) return ;
regionId [ i ] = id ;
dist [ i ] = 0 ;
heap . push ( { i , f : 0 } ) ;
} ) ;
let guard = 0 ;
while ( heap . length > 0 && guard ++ < SIZE * 16 ) {
const cur = heap . pop ( ) ;
if ( ! cur || cur . f > dist [ cur . i ] + 1e-5 ) continue ;
const [ cx , cy ] = xyOf ( cur . i ) ;
const curRegion = regionId [ cur . i ] ;
for ( const [ nx , ny , step ] of neighbors8 ( cx , cy ) ) {
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] ) continue ;
const ridge = Math . max ( ridgeField [ ni ] , ridgeField [ cur . i ] ) ;
const riverBarrier = Math . max ( river [ ni ] , river [ cur . i ] ) ;
const divide = ridge * 7.8 + Math . max ( 0 , elevation [ ni ] - 0.54 ) * 4.4 + slope [ ni ] * 3.8 ;
const watershed = Math . max ( 0 , flowAccum [ cur . i ] - flowAccum [ ni ] ) * 0.7 ;
const riverCost = riverBarrier > 0.72 ? 4.6 : riverBarrier > 0.35 ? 1.9 : 0 ;
const stepCost = Math . max ( 0.22 , 1 + divide + riverCost + watershed + Math . abs ( elevation [ ni ] - elevation [ cur . i ] ) * 3.2 ) * step ;
const nd = dist [ cur . i ] + stepCost ;
if ( nd < dist [ ni ] ) {
dist [ ni ] = nd ;
regionId [ ni ] = curRegion ;
heap . push ( { i : ni , f : nd } ) ;
}
}
}
return { regionId , centers } ;
}
2026-05-26 15:32:27 +09:00
function collectRegionComponents ( regionId , sea , id ) {
const seen = new Uint8Array ( SIZE ) ;
const components = [ ] ;
for ( let i = 0 ; i < SIZE ; i ++ ) {
if ( seen [ i ] || sea [ i ] || regionId [ i ] !== id ) continue ;
const queue = [ i ] ;
const cells = [ ] ;
seen [ i ] = 1 ;
for ( let q = 0 ; q < queue . length ; q ++ ) {
const cur = queue [ q ] ;
cells . push ( cur ) ;
const [ x , y ] = xyOf ( cur ) ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( seen [ ni ] || sea [ ni ] || regionId [ ni ] !== id ) continue ;
seen [ ni ] = 1 ;
queue . push ( ni ) ;
}
}
components . push ( cells ) ;
}
return components . sort ( ( a , b ) => b . length - a . length ) ;
}
function chooseRegionalReassignment ( cells , regionId , sea , centers , forbiddenId = - 1 ) {
const adjacent = new Map ( ) ;
let sx = 0 , sy = 0 ;
for ( const i of cells ) {
const [ x , y ] = xyOf ( i ) ;
sx += x ; sy += y ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
const id = regionId [ ni ] ;
if ( sea [ ni ] || id < 0 || id === forbiddenId ) continue ;
adjacent . set ( id , ( adjacent . get ( id ) || 0 ) + 1 ) ;
}
}
let bestId = - 1 ;
let bestScore = - INF ;
const cx = sx / Math . max ( 1 , cells . length ) ;
const cy = sy / Math . max ( 1 , cells . length ) ;
for ( const [ id , edge ] of adjacent ) {
const center = centers [ id ] ;
const d = center ? Math . hypot ( center . x - cx , center . y - cy ) : 0 ;
const score = edge * 4 - d * 0.04 + ( id === 0 ? - 1.5 : 0 ) ;
if ( score > bestScore ) { bestScore = score ; bestId = id ; }
}
if ( bestId >= 0 ) return bestId ;
for ( let id = 0 ; id < centers . length ; id ++ ) {
if ( id === forbiddenId || ! centers [ id ] ) continue ;
const d = Math . hypot ( centers [ id ] . x - cx , centers [ id ] . y - cy ) ;
const score = - d + ( id === 0 ? - 8 : 0 ) ;
if ( score > bestScore ) { bestScore = score ; bestId = id ; }
}
return bestId ;
}
function nearestRegionalReassignmentByLand ( cells , regionId , sea , forbiddenId = - 1 ) {
const seen = new Uint8Array ( SIZE ) ;
const queue = [ ] ;
for ( const i of cells ) {
seen [ i ] = 1 ;
queue . push ( i ) ;
}
for ( let q = 0 ; q < queue . length ; q ++ ) {
const cur = queue [ q ] ;
const [ x , y ] = xyOf ( cur ) ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] || seen [ ni ] ) continue ;
const id = regionId [ ni ] ;
if ( id >= 0 && id !== forbiddenId ) return id ;
seen [ ni ] = 1 ;
queue . push ( ni ) ;
}
}
return - 1 ;
}
function repairDisconnectedRegionalPrefectures ( regionId , sea , centers , anchorMask ) {
let totalChanged = 0 ;
for ( let pass = 0 ; pass < 10 ; pass ++ ) {
let changedThisPass = 0 ;
const ids = [ ... new Set ( [ ... regionId ] . filter ( ( id , i ) => id >= 0 && ! sea [ i ] ) ) ] . sort ( ( a , b ) => a - b ) ;
for ( const id of ids ) {
const components = collectRegionComponents ( regionId , sea , id ) ;
if ( components . length <= 1 ) continue ;
let keepIndex = 0 ;
if ( id === 0 ) {
const anchorIndex = components . findIndex ( ( cells ) => cells . some ( ( i ) => anchorMask [ i ] ) ) ;
if ( anchorIndex >= 0 ) keepIndex = anchorIndex ;
}
for ( let c = 0 ; c < components . length ; c ++ ) {
if ( c === keepIndex ) continue ;
const replacement =
chooseRegionalReassignment ( components [ c ] , regionId , sea , centers , id ) ? ?
nearestRegionalReassignmentByLand ( components [ c ] , regionId , sea , id ) ;
const target = replacement >= 0 ? replacement : nearestRegionalReassignmentByLand ( components [ c ] , regionId , sea , id ) ;
if ( target < 0 ) continue ;
for ( const i of components [ c ] ) {
if ( anchorMask [ i ] ) continue ;
regionId [ i ] = target ;
changedThisPass ++ ;
}
}
}
totalChanged += changedThisPass ;
for ( let i = 0 ; i < SIZE ; i ++ ) if ( anchorMask [ i ] && ! sea [ i ] ) regionId [ i ] = 0 ;
if ( changedThisPass === 0 ) break ;
}
return totalChanged ;
}
function componentTouchesOutside ( cells , sea ) {
for ( const i of cells ) {
const [ x , y ] = xyOf ( i ) ;
if ( x === 0 || y === 0 || x === MAP _W - 1 || y === MAP _H - 1 ) return true ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) if ( sea [ indexOf ( nx , ny ) ] ) return true ;
}
return false ;
}
function boundaryNeighborIds ( cells , regionId , sea , ownId ) {
const ids = new Set ( ) ;
for ( const i of cells ) {
const [ x , y ] = xyOf ( i ) ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
const id = regionId [ ni ] ;
if ( ! sea [ ni ] && id >= 0 && id !== ownId ) ids . add ( id ) ;
}
}
return ids ;
}
function countRegionalEnclaves ( regionId , sea ) {
let count = 0 ;
const ids = [ ... new Set ( [ ... regionId ] . filter ( ( id , i ) => id >= 0 && ! sea [ i ] ) ) ] . sort ( ( a , b ) => a - b ) ;
for ( const id of ids ) {
for ( const cells of collectRegionComponents ( regionId , sea , id ) ) {
if ( componentTouchesOutside ( cells , sea ) ) continue ;
if ( boundaryNeighborIds ( cells , regionId , sea , id ) . size === 1 ) count ++ ;
}
}
return count ;
}
function carveRegionalCorridor ( regionId , sea , cells , ownId , enclosingId , naturalBarrierScore ) {
const best = new Float32Array ( SIZE ) ;
const cameFrom = new Int32Array ( SIZE ) ;
best . fill ( INF ) ;
cameFrom . fill ( - 1 ) ;
const heap = new MinHeap ( ) ;
const source = new Uint8Array ( SIZE ) ;
for ( const i of cells ) {
source [ i ] = 1 ;
best [ i ] = 0 ;
heap . push ( { i , f : 0 } ) ;
}
let target = - 1 ;
while ( heap . length ) {
const cur = heap . pop ( ) ;
if ( ! cur || cur . f > best [ cur . i ] + 1e-5 ) continue ;
const [ x , y ] = xyOf ( cur . i ) ;
if ( ! source [ cur . i ] ) {
if ( x === 0 || y === 0 || x === MAP _W - 1 || y === MAP _H - 1 ) { target = cur . i ; break ; }
let seaAdjacent = false ;
let otherAdjacent = false ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] ) seaAdjacent = true ;
else if ( regionId [ ni ] >= 0 && regionId [ ni ] !== ownId && regionId [ ni ] !== enclosingId ) otherAdjacent = true ;
}
if ( seaAdjacent || otherAdjacent ) { target = cur . i ; break ; }
}
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] || regionId [ ni ] === ownId ) continue ;
const id = regionId [ ni ] ;
const regionPenalty = id === enclosingId ? 0 : 8 ;
const barrier = naturalBarrierScore ? . [ ni ] || 0 ;
const nd = cur . f + 1 + barrier * 3.2 + regionPenalty ;
if ( nd < best [ ni ] ) {
best [ ni ] = nd ;
cameFrom [ ni ] = cur . i ;
heap . push ( { i : ni , f : nd } ) ;
}
}
}
if ( target < 0 ) return 0 ;
let changed = 0 ;
for ( let i = target ; i >= 0 && regionId [ i ] !== ownId ; i = cameFrom [ i ] ) {
if ( ! sea [ i ] && regionId [ i ] !== ownId ) {
regionId [ i ] = ownId ;
changed ++ ;
}
if ( cameFrom [ i ] < 0 ) break ;
}
return changed ;
}
function repairRegionalEnclaves ( regionId , sea , centers , anchorMask , naturalBarrierScore ) {
let changed = 0 ;
const ids = [ ... new Set ( [ ... regionId ] . filter ( ( id , i ) => id >= 0 && ! sea [ i ] ) ) ] . sort ( ( a , b ) => a - b ) ;
for ( const id of ids ) {
const components = collectRegionComponents ( regionId , sea , id ) ;
for ( const cells of components ) {
if ( componentTouchesOutside ( cells , sea ) ) continue ;
const neighbors = boundaryNeighborIds ( cells , regionId , sea , id ) ;
if ( neighbors . size !== 1 ) continue ;
const enclosingId = [ ... neighbors ] [ 0 ] ;
const protectedAnchor = id === 0 && cells . some ( ( i ) => anchorMask [ i ] ) ;
if ( protectedAnchor ) {
changed += carveRegionalCorridor ( regionId , sea , cells , id , enclosingId , naturalBarrierScore ) ;
} else {
for ( const i of cells ) {
if ( anchorMask [ i ] ) continue ;
regionId [ i ] = enclosingId ;
changed ++ ;
}
}
}
}
for ( let i = 0 ; i < SIZE ; i ++ ) if ( anchorMask [ i ] && ! sea [ i ] ) regionId [ i ] = 0 ;
return changed ;
}
function repairFinalRegionalTopology ( regionId , sea , centers , anchorMask , naturalBarrierScore ) {
let totalChanged = 0 ;
for ( let pass = 0 ; pass < 12 ; pass ++ ) {
const disconnected = repairDisconnectedRegionalPrefectures ( regionId , sea , centers , anchorMask ) ;
const enclaves = repairRegionalEnclaves ( regionId , sea , centers , anchorMask , naturalBarrierScore ) ;
totalChanged += disconnected + enclaves ;
if ( disconnected + enclaves === 0 ) break ;
}
return totalChanged ;
}
function regionalAreaById ( regionId , sea ) {
const area = new Map ( ) ;
for ( let i = 0 ; i < SIZE ; i ++ ) if ( ! sea [ i ] && regionId [ i ] >= 0 ) area . set ( regionId [ i ] , ( area . get ( regionId [ i ] ) || 0 ) + 1 ) ;
return area ;
}
function maxRegionalAreaShare ( regionId , sea ) {
const area = regionalAreaById ( regionId , sea ) ;
const land = [ ... area . values ( ) ] . reduce ( ( sum , value ) => sum + value , 0 ) ;
return land ? Math . max ( 0 , ... area . values ( ) ) / land : 0 ;
}
function canMoveRegionalBoundaryCell ( regionId , sea , i , ownId ) {
const [ x , y ] = xyOf ( i ) ;
let ownNeighbors = 0 ;
let otherNeighbors = 0 ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] ) continue ;
if ( regionId [ ni ] === ownId ) ownNeighbors ++ ;
else if ( regionId [ ni ] >= 0 ) otherNeighbors ++ ;
}
return ownNeighbors >= 2 && otherNeighbors > 0 ;
}
function rebalanceOversizedRegionalPrefectures ( regionId , sea , centers , anchorMask , naturalBarrierScore ) {
const minArea = 720 ;
for ( let pass = 0 ; pass < 4 ; pass ++ ) {
const area = regionalAreaById ( regionId , sea ) ;
const land = [ ... area . values ( ) ] . reduce ( ( sum , value ) => sum + value , 0 ) ;
const maxArea = Math . max ( minArea * 2 , Math . floor ( land * 0.32 ) ) ;
let changed = 0 ;
const oversized = [ ... area . entries ( ) ] . filter ( ( [ , value ] ) => value > maxArea ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) ;
for ( const [ id ] of oversized ) {
const candidates = [ ] ;
for ( let y = 1 ; y < MAP _H - 1 ; y ++ ) {
for ( let x = 1 ; x < MAP _W - 1 ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] || regionId [ i ] !== id || anchorMask [ i ] || ! canMoveRegionalBoundaryCell ( regionId , sea , i , id ) ) continue ;
const counts = new Map ( ) ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
const other = regionId [ ni ] ;
if ( ! sea [ ni ] && other >= 0 && other !== id && ( area . get ( other ) || 0 ) < maxArea ) counts . set ( other , ( counts . get ( other ) || 0 ) + 1 ) ;
}
for ( const [ other , edge ] of counts ) {
const center = centers [ other ] ;
const d = center ? Math . hypot ( center . x - x , center . y - y ) : 0 ;
candidates . push ( { i , other , score : edge * 2.0 + ( naturalBarrierScore ? . [ i ] || 0 ) * 1.4 - d * 0.006 } ) ;
}
}
}
candidates . sort ( ( a , b ) => b . score - a . score || a . i - b . i ) ;
for ( const candidate of candidates ) {
if ( ( area . get ( id ) || 0 ) <= maxArea ) break ;
if ( ( area . get ( candidate . other ) || 0 ) >= maxArea || regionId [ candidate . i ] !== id ) continue ;
regionId [ candidate . i ] = candidate . other ;
area . set ( id , ( area . get ( id ) || 0 ) - 1 ) ;
area . set ( candidate . other , ( area . get ( candidate . other ) || 0 ) + 1 ) ;
changed ++ ;
}
}
repairDisconnectedRegionalPrefectures ( regionId , sea , centers , anchorMask ) ;
if ( ! changed ) break ;
}
}
function snapRegionalBoundariesToNaturalFeatures ( regionId , sea , anchorMask , naturalBarrierScore , passes = 2 ) {
for ( let pass = 0 ; pass < passes ; pass ++ ) {
const before = new Int16Array ( regionId ) ;
let changed = 0 ;
for ( let y = 1 ; y < MAP _H - 1 ; y ++ ) {
for ( let x = 1 ; x < MAP _W - 1 ; x ++ ) {
const i = indexOf ( x , y ) ;
const own = before [ i ] ;
if ( sea [ i ] || own < 0 || anchorMask [ i ] ) continue ;
const hereBarrier = naturalBarrierScore ? . [ i ] || 0 ;
if ( hereBarrier > 0.54 || ! canMoveRegionalBoundaryCell ( before , sea , i , own ) ) continue ;
const counts = new Map ( ) ;
let bestNeighborBarrier = 0 ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
const other = before [ ni ] ;
if ( sea [ ni ] || other < 0 ) continue ;
bestNeighborBarrier = Math . max ( bestNeighborBarrier , naturalBarrierScore ? . [ ni ] || 0 ) ;
if ( other !== own ) counts . set ( other , ( counts . get ( other ) || 0 ) + 1 ) ;
}
if ( counts . size === 0 || bestNeighborBarrier < hereBarrier + 0.18 ) continue ;
let target = - 1 , best = - 1 ;
for ( const [ other , count ] of counts ) if ( count > best ) { best = count ; target = other ; }
if ( target >= 0 && best >= 2 ) {
regionId [ i ] = target ;
changed ++ ;
}
}
}
if ( ! changed ) break ;
}
}
function mergeTinyRegionalPrefectures ( regionId , sea , centers , anchorMask , minArea ) {
const ids = [ ... new Set ( [ ... regionId ] . filter ( ( id , i ) => id >= 0 && ! sea [ i ] ) ) ] . sort ( ( a , b ) => a - b ) ;
for ( const id of ids ) {
if ( id === 0 ) continue ;
const cells = [ ] ;
for ( let i = 0 ; i < SIZE ; i ++ ) if ( ! sea [ i ] && regionId [ i ] === id ) cells . push ( i ) ;
if ( cells . length === 0 || cells . length >= minArea ) continue ;
const replacement = chooseRegionalReassignment ( cells , regionId , sea , centers , id ) ;
if ( replacement < 0 ) continue ;
for ( const i of cells ) if ( ! anchorMask [ i ] ) regionId [ i ] = replacement ;
}
}
2026-05-21 13:20:19 +09:00
function buildRegionalNaturalBarrierScore ( sea , elevation , slope , river , ridgeField , flowAccum ) {
const score = new Float32Array ( SIZE ) ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] ) continue ;
let coast = 0 ;
for ( const [ nx , ny ] of neighbors8 ( x , y ) ) if ( sea [ indexOf ( nx , ny ) ] ) coast = 1 ;
const highRidge = clamp ( ridgeField [ i ] * 1.75 + Math . max ( 0 , elevation [ i ] - 0.56 ) * 0.72 ) ;
const slopeBreak = clamp ( slope [ i ] * 0.92 + Math . max ( 0 , slope [ i ] - 0.32 ) * 0.80 ) ;
const majorRiver = clamp ( Math . max ( 0 , river [ i ] - 0.26 ) * 1.85 + Math . max ( 0 , flowAccum [ i ] - 0.36 ) * 0.86 ) ;
const watershedDivide = clamp ( ridgeField [ i ] * Math . max ( 0 , 0.62 - flowAccum [ i ] ) * 1.08 + Math . max ( 0 , elevation [ i ] - 0.50 ) * slope [ i ] * 0.72 ) ;
score [ i ] = clamp ( highRidge * 0.88 + slopeBreak * 0.48 + majorRiver * 0.82 + watershedDivide * 0.58 + coast * 0.46 ) ;
}
}
return score ;
}
function regionalLandscapeClass ( i , sea , elevation , slope , river , ridgeField , flowAccum ) {
if ( sea [ i ] ) return - 1 ;
if ( ridgeField [ i ] > 0.56 || elevation [ i ] > 0.68 ) return 1 ;
if ( river [ i ] > 0.44 || flowAccum [ i ] > 0.58 ) return 2 ;
if ( slope [ i ] > 0.42 || ( ridgeField [ i ] > 0.36 && elevation [ i ] > 0.52 ) ) return 3 ;
if ( elevation [ i ] < 0.36 && slope [ i ] < 0.20 ) return 4 ;
if ( elevation [ i ] < 0.48 && flowAccum [ i ] > 0.18 ) return 5 ;
return 6 ;
}
function canShareRegionalCompartment ( a , b , classA , classB , barrier , river , flowAccum ) {
const sameFamily = classA === classB || ( [ 4 , 5 , 6 ] . includes ( classA ) && [ 4 , 5 , 6 ] . includes ( classB ) ) ;
if ( ! sameFamily ) return false ;
const majorRiver = Math . max ( river [ a ] , river [ b ] ) > 0.58 || Math . max ( flowAccum [ a ] , flowAccum [ b ] ) > 0.72 ;
const threshold = classA === 1 || classB === 1 ? 0.38 : classA === 2 || classB === 2 ? 0.52 : 0.62 ;
return barrier < threshold && ! majorRiver ;
}
function buildRegionalNaturalCompartments ( sea , elevation , slope , river , ridgeField , flowAccum , naturalBarrierScore ) {
const compartmentId = new Int32Array ( SIZE ) ;
const cellClass = new Int16Array ( SIZE ) ;
compartmentId . fill ( - 1 ) ;
cellClass . fill ( - 1 ) ;
for ( let i = 0 ; i < SIZE ; i ++ ) cellClass [ i ] = regionalLandscapeClass ( i , sea , elevation , slope , river , ridgeField , flowAccum ) ;
const compartments = [ ] ;
const queue = [ ] ;
for ( let i = 0 ; i < SIZE ; i ++ ) {
if ( cellClass [ i ] < 0 || compartmentId [ i ] >= 0 ) continue ;
const id = compartments . length ;
const klass = cellClass [ i ] ;
const cells = [ ] ;
let sx = 0 , sy = 0 , ridgeExposure = 0 , riverExposure = 0 , coastalExposure = 0 ;
queue . length = 0 ;
queue . push ( i ) ;
compartmentId [ i ] = id ;
for ( let q = 0 ; q < queue . length ; q ++ ) {
const cur = queue [ q ] ;
const [ x , y ] = xyOf ( cur ) ;
cells . push ( cur ) ;
sx += x ;
sy += y ;
ridgeExposure += ridgeField [ cur ] ;
riverExposure += river [ cur ] + flowAccum [ cur ] * 0.45 ;
let coast = 0 ;
for ( const [ nx , ny ] of neighbors8 ( x , y ) ) if ( sea [ indexOf ( nx , ny ) ] ) coast = 1 ;
coastalExposure += coast ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( compartmentId [ ni ] >= 0 || cellClass [ ni ] < 0 ) continue ;
const barrier = ( naturalBarrierScore [ cur ] + naturalBarrierScore [ ni ] ) * 0.5 ;
if ( ! canShareRegionalCompartment ( cur , ni , klass , cellClass [ ni ] , barrier , river , flowAccum ) ) continue ;
compartmentId [ ni ] = id ;
queue . push ( ni ) ;
}
}
const area = cells . length ;
compartments . push ( {
id ,
cells ,
area ,
classId : klass ,
x : sx / Math . max ( 1 , area ) ,
y : sy / Math . max ( 1 , area ) ,
ridgeExposure : ridgeExposure / Math . max ( 1 , area ) ,
riverExposure : riverExposure / Math . max ( 1 , area ) ,
coastalExposure : coastalExposure / Math . max ( 1 , area ) ,
2026-05-21 22:03:14 +09:00
adjacent : new Map ( ) ,
2026-05-21 13:20:19 +09:00
} ) ;
}
2026-05-21 22:03:14 +09:00
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] ) continue ;
const a = compartmentId [ i ] ;
if ( a < 0 || ! compartments [ a ] ) continue ;
for ( const [ nx , ny ] of [ [ x + 1 , y ] , [ x , y + 1 ] ] ) {
if ( ! inside ( nx , ny ) ) continue ;
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] ) continue ;
const b = compartmentId [ ni ] ;
if ( b < 0 || a === b || ! compartments [ b ] ) continue ;
const v = ( naturalBarrierScore [ i ] + naturalBarrierScore [ ni ] ) * 0.5 ;
const edgeA = compartments [ a ] . adjacent . get ( b ) || { count : 0 , target : 0 } ;
edgeA . count ++ ;
edgeA . target += v ;
compartments [ a ] . adjacent . set ( b , edgeA ) ;
const edgeB = compartments [ b ] . adjacent . get ( a ) || { count : 0 , target : 0 } ;
edgeB . count ++ ;
edgeB . target += v ;
compartments [ b ] . adjacent . set ( a , edgeB ) ;
}
}
}
2026-05-21 13:20:19 +09:00
return { compartmentId , compartments } ;
}
function countRegionBorderEdges ( regionId , sea ) {
let count = 0 ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] || regionId [ i ] < 0 ) continue ;
for ( const [ nx , ny ] of [ [ x + 1 , y ] , [ x , y + 1 ] ] ) {
if ( ! inside ( nx , ny ) ) continue ;
const ni = indexOf ( nx , ny ) ;
if ( ! sea [ ni ] && regionId [ ni ] >= 0 && regionId [ ni ] !== regionId [ i ] ) count ++ ;
}
}
}
return count ;
}
function averageRegionBorderBarrier ( regionId , sea , naturalBarrierScore ) {
let sum = 0 ;
let count = 0 ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] || regionId [ i ] < 0 ) continue ;
for ( const [ nx , ny ] of [ [ x + 1 , y ] , [ x , y + 1 ] ] ) {
if ( ! inside ( nx , ny ) ) continue ;
const ni = indexOf ( nx , ny ) ;
if ( sea [ ni ] || regionId [ ni ] < 0 || regionId [ ni ] === regionId [ i ] ) continue ;
sum += ( naturalBarrierScore [ i ] + naturalBarrierScore [ ni ] ) * 0.5 ;
count ++ ;
}
}
}
return count ? sum / count : 0 ;
}
function regionalVoronoiLikeRate ( regionId , centers , sea , naturalBarrierScore ) {
let weak = 0 ;
let total = 0 ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] || regionId [ i ] < 0 ) continue ;
for ( const [ nx , ny ] of [ [ x + 1 , y ] , [ x , y + 1 ] ] ) {
if ( ! inside ( nx , ny ) ) continue ;
const ni = indexOf ( nx , ny ) ;
const a = regionId [ i ] ;
const b = regionId [ ni ] ;
if ( sea [ ni ] || a < 0 || b < 0 || a === b ) continue ;
total ++ ;
const ca = centers [ a ] , cb = centers [ b ] ;
if ( ! ca || ! cb ) continue ;
const mx = ( x + nx ) * 0.5 ;
const my = ( y + ny ) * 0.5 ;
const nearBisector = Math . abs ( Math . hypot ( mx - ca . x , my - ca . y ) - Math . hypot ( mx - cb . x , my - cb . y ) ) < 4.5 ;
if ( nearBisector && ( naturalBarrierScore [ i ] + naturalBarrierScore [ ni ] ) * 0.5 < 0.36 ) weak ++ ;
}
}
}
return total ? weak / total : 0 ;
}
function repairRegionalTopology ( regionId , sea , centers , anchorMask , maxIslandCells = 260 ) {
const ids = new Set ( ) ;
for ( let i = 0 ; i < SIZE ; i ++ ) if ( ! sea [ i ] && regionId [ i ] >= 0 ) ids . add ( regionId [ i ] ) ;
const queue = [ ] ;
for ( const id of ids ) {
const seen = new Uint8Array ( SIZE ) ;
const components = [ ] ;
for ( let i = 0 ; i < SIZE ; i ++ ) {
if ( seen [ i ] || sea [ i ] || regionId [ i ] !== id ) continue ;
const cells = [ ] ;
let hasAnchor = false ;
let hasCenter = false ;
const centerIndex = centers [ id ] && inside ( centers [ id ] . x , centers [ id ] . y ) ? indexOf ( centers [ id ] . x , centers [ id ] . y ) : - 1 ;
queue . length = 0 ;
queue . push ( i ) ;
seen [ i ] = 1 ;
for ( let q = 0 ; q < queue . length ; q ++ ) {
const cur = queue [ q ] ;
cells . push ( cur ) ;
if ( anchorMask [ cur ] ) hasAnchor = true ;
if ( cur === centerIndex ) hasCenter = true ;
const [ x , y ] = xyOf ( cur ) ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
if ( seen [ ni ] || sea [ ni ] || regionId [ ni ] !== id ) continue ;
seen [ ni ] = 1 ;
queue . push ( ni ) ;
}
}
components . push ( { cells , hasAnchor , hasCenter } ) ;
}
if ( components . length <= 1 ) continue ;
components . sort ( ( a , b ) => ( b . hasAnchor ? 2000000 : 0 ) + ( b . hasCenter ? 1000000 : 0 ) + b . cells . length - ( ( a . hasAnchor ? 2000000 : 0 ) + ( a . hasCenter ? 1000000 : 0 ) + a . cells . length ) ) ;
for ( const comp of components . slice ( 1 ) ) {
const counts = new Map ( ) ;
for ( const ci of comp . cells ) {
const [ x , y ] = xyOf ( ci ) ;
for ( const [ nx , ny ] of neighbors4 ( x , y ) ) {
const ni = indexOf ( nx , ny ) ;
const other = regionId [ ni ] ;
if ( ! sea [ ni ] && other >= 0 && other !== id ) counts . set ( other , ( counts . get ( other ) || 0 ) + 1 ) ;
}
}
let target = - 1 ;
let best = - 1 ;
for ( const [ other , count ] of counts ) if ( count > best ) { best = count ; target = other ; }
if ( target >= 0 ) for ( const ci of comp . cells ) if ( ! anchorMask [ ci ] ) regionId [ ci ] = target ;
}
}
for ( let i = 0 ; i < SIZE ; i ++ ) if ( anchorMask [ i ] && ! sea [ i ] ) regionId [ i ] = 0 ;
}
2026-05-26 15:32:27 +09:00
export function extractRegionBorderSegments ( regionId , sea , options = { } ) {
2026-05-21 13:20:19 +09:00
const segments = [ ] ;
2026-05-26 15:32:27 +09:00
const nameById = options . prefectureRegions
? new Map ( options . prefectureRegions . map ( ( region ) => [ region . id , region . name ] ) )
: null ;
const fail = ( message ) => {
if ( options . throwOnInvalid ) throw new Error ( message ) ;
if ( options . logInvalid ) console . error ( message ) ;
} ;
2026-05-21 13:20:19 +09:00
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( sea [ i ] || regionId [ i ] < 0 ) continue ;
const a = regionId [ i ] ;
if ( x + 1 < MAP _W && ! sea [ indexOf ( x + 1 , y ) ] ) {
2026-05-26 15:32:27 +09:00
const ni = indexOf ( x + 1 , y ) ;
const b = regionId [ ni ] ;
2026-05-21 13:20:19 +09:00
if ( b >= 0 && a !== b ) segments . push ( [ [ x + 1 , y ] , [ x + 1 , y + 1 ] ] ) ;
2026-05-26 15:32:27 +09:00
else if ( options . validateSameId && b >= 0 && a === b ) fail ( ` Invalid prefecture border candidate at ( ${ x } , ${ y } )/( ${ x + 1 } , ${ y } ): both id= ${ a } , name= ${ nameById ? . get ( a ) || "-" } ` ) ;
2026-05-21 13:20:19 +09:00
}
if ( y + 1 < MAP _H && ! sea [ indexOf ( x , y + 1 ) ] ) {
2026-05-26 15:32:27 +09:00
const ni = indexOf ( x , y + 1 ) ;
const b = regionId [ ni ] ;
2026-05-21 13:20:19 +09:00
if ( b >= 0 && a !== b ) segments . push ( [ [ x , y + 1 ] , [ x + 1 , y + 1 ] ] ) ;
2026-05-26 15:32:27 +09:00
else if ( options . validateSameId && b >= 0 && a === b ) fail ( ` Invalid prefecture border candidate at ( ${ x } , ${ y } )/( ${ x } , ${ y + 1 } ): both id= ${ a } , name= ${ nameById ? . get ( a ) || "-" } ` ) ;
}
}
}
if ( options . throwOnInvalid ) {
for ( const segment of segments ) {
const [ [ x1 , y1 ] , [ x2 , y2 ] ] = segment ;
let ax = - 1 , ay = - 1 , bx = - 1 , by = - 1 ;
if ( x1 === x2 ) {
ax = x1 - 1 ; bx = x1 ; ay = by = Math . min ( y1 , y2 ) ;
} else if ( y1 === y2 ) {
ax = bx = Math . min ( x1 , x2 ) ; ay = y1 - 1 ; by = y1 ;
}
if ( ! inside ( ax , ay ) || ! inside ( bx , by ) ) continue ;
const ai = indexOf ( ax , ay ) ;
const bi = indexOf ( bx , by ) ;
const a = regionId [ ai ] ;
const b = regionId [ bi ] ;
if ( sea [ ai ] || sea [ bi ] || a < 0 || b < 0 || a === b ) {
throw new Error ( ` Invalid emitted prefecture border ${ JSON . stringify ( segment ) } between ( ${ ax } , ${ ay } ) id= ${ a } name= ${ nameById ? . get ( a ) || "-" } and ( ${ bx } , ${ by } ) id= ${ b } name= ${ nameById ? . get ( b ) || "-" } ` ) ;
2026-05-21 13:20:19 +09:00
}
}
}
return segments ;
}
export function extractMaskBorder ( mask , sea = null ) {
const segments = [ ] ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
const a = mask [ i ] ;
if ( x + 1 < MAP _W ) {
const ni = indexOf ( x + 1 , y ) ;
const b = mask [ ni ] ;
if ( a !== b && ! ( sea && ( sea [ i ] || sea [ ni ] ) ) ) segments . push ( [ [ x + 1 , y ] , [ x + 1 , y + 1 ] ] ) ;
}
if ( y + 1 < MAP _H ) {
const ni = indexOf ( x , y + 1 ) ;
const b = mask [ ni ] ;
if ( a !== b && ! ( sea && ( sea [ i ] || sea [ ni ] ) ) ) segments . push ( [ [ x , y + 1 ] , [ x + 1 , y + 1 ] ] ) ;
}
}
}
return segments ;
}
export function extractAdminBorderSegments ( adminId , prefectureMask ) {
const segments = [ ] ;
for ( let y = 0 ; y < MAP _H ; y ++ ) {
for ( let x = 0 ; x < MAP _W ; x ++ ) {
const i = indexOf ( x , y ) ;
if ( ! prefectureMask [ i ] ) continue ;
const a = adminId [ i ] ;
if ( a < 0 ) continue ;
if ( x + 1 < MAP _W && prefectureMask [ indexOf ( x + 1 , y ) ] ) {
const b = adminId [ indexOf ( x + 1 , y ) ] ;
if ( b >= 0 && a !== b ) segments . push ( [ [ x + 1 , y ] , [ x + 1 , y + 1 ] ] ) ;
}
if ( y + 1 < MAP _H && prefectureMask [ indexOf ( x , y + 1 ) ] ) {
const b = adminId [ indexOf ( x , y + 1 ) ] ;
if ( b >= 0 && a !== b ) segments . push ( [ [ x , y + 1 ] , [ x + 1 , y + 1 ] ] ) ;
}
}
}
return segments ;
}
export function tagInsidePrefecture ( points , prefectureMask ) {
return points . map ( ( p ) => ( { ... p , insidePrefecture : Boolean ( prefectureMask [ indexOf ( p . x , p . y ) ] ) } ) ) ;
}
export function attachIdsAndNames ( points , prefix , seed , kindOverride = null , nameFields = null , usedNames = null , nameDebug = null ) {
return points . map ( ( p , i ) => {
const id = ` ${ prefix } - ${ i } ` ;
const kind = kindOverride || p . kind ;
2026-05-28 19:37:28 +09:00
if ( prefix === "logistics" ) {
return {
... p ,
id ,
name : null ,
facilityLabel : p . facilityLabel || "Logistics Park" ,
kind ,
labelStyle : "facility" ,
suppressSettlementLabel : true ,
insidePrefecture : Boolean ( p . insidePrefecture ) ,
} ;
}
2026-05-21 13:20:19 +09:00
const name = generateEntityName ( seed + prefix . length * 1000 , id , { ... p , kind } , nameFields , usedNames , nameDebug ) ;
if ( usedNames ) usedNames . add ( name ) ;
return {
... p ,
id ,
name ,
2026-05-22 13:57:52 +09:00
kind ,
2026-05-21 13:20:19 +09:00
insidePrefecture : Boolean ( p . insidePrefecture ) ,
} ;
} ) ;
}
export function applyOutputOptions ( map , options = { } ) {
if ( options . includeDebugFields !== false ) return map ;
const slim = { ... map } ;
delete slim . settlementCluster ;
delete slim . ridgeField ;
delete slim . valleyField ;
delete slim . basinField ;
delete slim . coastalLowland ;
delete slim . flowAccum ;
delete slim . erosionField ;
delete slim . depositionField ;
2026-05-21 22:03:14 +09:00
delete slim . terrainTemplate ;
delete slim . ocean ;
delete slim . lake ;
delete slim . arcSpineField ;
delete slim . branchRidgeField ;
delete slim . depositionalLowland ;
delete slim . alluvialFanField ;
delete slim . deltaField ;
delete slim . naturalBarrierScore ;
2026-05-21 13:20:19 +09:00
return slim ;
}