diff --git a/adminRegions.js b/adminRegions.js index fdbd166..0339e0c 100644 --- a/adminRegions.js +++ b/adminRegions.js @@ -1,6 +1,4 @@ -import { MinHeap } from "./graph.js"; -import { INF, MAP_H, MAP_W, SIZE, clamp, indexOf, inside, xyOf } from "./grid.js"; -import { weightedScore } from "./scoring.js"; +import { INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, indexOf, inside, weightedScore, xyOf } from "./mapUtils.js"; function neighbors8(x, y) { const out = []; diff --git a/entitySelection.js b/entitySelection.js deleted file mode 100644 index 21fa8e2..0000000 --- a/entitySelection.js +++ /dev/null @@ -1,17 +0,0 @@ -import { hash2 } from "./random.js"; - -export function pickEntities(candidates, { max = 99, minDistance = 5, threshold = 0, seed = 0, jitter = 0.04 } = {}) { - const sorted = candidates - .filter((p) => Number.isFinite(p.score) && p.score >= threshold) - .map((p) => ({ ...p, score: p.score + hash2(p.x, p.y, seed + 54321) * jitter })) - .sort((a, b) => b.score - a.score); - - const out = []; - for (const candidate of sorted) { - if (out.every((p) => Math.hypot(p.x - candidate.x, p.y - candidate.y) >= minDistance)) { - out.push(candidate); - if (out.length >= max) break; - } - } - return out; -} diff --git a/fields.js b/fields.js deleted file mode 100644 index 593080c..0000000 --- a/fields.js +++ /dev/null @@ -1,28 +0,0 @@ -import { SIZE } from "./grid.js"; - -export function createMapFields() { - const flowTo = new Int32Array(SIZE); - flowTo.fill(-1); - - return { - elevation: new Float32Array(SIZE), - moisture: new Float32Array(SIZE), - slope: new Float32Array(SIZE), - sea: new Uint8Array(SIZE), - river: new Float32Array(SIZE), - floodplain: new Float32Array(SIZE), - plain: new Float32Array(SIZE), - agriculture: new Float32Array(SIZE), - ridgeField: new Float32Array(SIZE), - valleyField: new Float32Array(SIZE), - basinField: new Float32Array(SIZE), - coastalLowland: new Float32Array(SIZE), - flowAccum: new Float32Array(SIZE), - erosionField: new Float32Array(SIZE), - depositionField: new Float32Array(SIZE), - flowTo, - portSuitability: new Float32Array(SIZE), - crossingSuitability: new Float32Array(SIZE), - passSuitability: new Float32Array(SIZE), - }; -} diff --git a/graph.js b/graph.js deleted file mode 100644 index 1311ca0..0000000 --- a/graph.js +++ /dev/null @@ -1,34 +0,0 @@ -export class MinHeap { - constructor() { this.items = []; } - push(item) { - this.items.push(item); - let i = this.items.length - 1; - while (i > 0) { - const parent = (i - 1) >> 1; - if (this.items[parent].f <= item.f) break; - this.items[i] = this.items[parent]; - i = parent; - } - this.items[i] = item; - } - pop() { - if (this.items.length === 0) return null; - const root = this.items[0]; - const last = this.items.pop(); - if (this.items.length > 0) { - let i = 0; - while (true) { - const left = i * 2 + 1; - const right = left + 1; - if (left >= this.items.length) break; - const child = right < this.items.length && this.items[right].f < this.items[left].f ? right : left; - if (this.items[child].f >= last.f) break; - this.items[i] = this.items[child]; - i = child; - } - this.items[i] = last; - } - return root; - } - get length() { return this.items.length; } -} diff --git a/grid.js b/grid.js deleted file mode 100644 index 355da28..0000000 --- a/grid.js +++ /dev/null @@ -1,26 +0,0 @@ -export const MAP_W = 172; -export const MAP_H = 122; -export const CELL_SIZE = 6; - -export const SIZE = MAP_W * MAP_H; -export const INF = 1e9; - -export function indexOf(x, y) { - return y * MAP_W + x; -} - -export function xyOf(i) { - return [i % MAP_W, Math.floor(i / MAP_W)]; -} - -export function inside(x, y) { - return x >= 0 && y >= 0 && x < MAP_W && y < MAP_H; -} - -export function clamp(v, a = 0, b = 1) { - return Math.max(a, Math.min(b, v)); -} - -export function nearMapEdge(x, y, margin = 1) { - return x < margin || y < margin || x >= MAP_W - margin || y >= MAP_H - margin; -} diff --git a/mapGenerator.js b/mapGenerator.js index 60da722..e1911d9 100644 --- a/mapGenerator.js +++ b/mapGenerator.js @@ -8,13 +8,9 @@ import { smoothAdminRegionsTerrainAware, snapAdminBoundariesToTerrain, } from "./adminRegions.js"; -import { pickEntities } from "./entitySelection.js"; -import { createMapFields } from "./fields.js"; -import { MinHeap } from "./graph.js"; -import { CELL_SIZE, INF, MAP_H, MAP_W, SIZE, clamp, indexOf, inside, nearMapEdge, xyOf } from "./grid.js"; -import { fbm, hash2, lerp, rand, smoothstep, valueNoise } from "./random.js"; +import { CELL_SIZE, INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, createMapFields, fbm, hash2, indexOf, inside, lerp, nearMapEdge, pickEntities, rand, smoothstep, valueNoise, xyOf } from "./mapUtils.js"; -export { CELL_SIZE, MAP_H, MAP_W, indexOf } from "./grid.js"; +export { CELL_SIZE, MAP_H, MAP_W, indexOf } from "./mapUtils.js"; function neighbors8(x, y) { const out = []; diff --git a/mapUtils.js b/mapUtils.js new file mode 100644 index 0000000..cc5b8ef --- /dev/null +++ b/mapUtils.js @@ -0,0 +1,167 @@ +export const MAP_W = 172; +export const MAP_H = 122; +export const CELL_SIZE = 6; + +export const SIZE = MAP_W * MAP_H; +export const INF = 1e9; + +export function indexOf(x, y) { + return y * MAP_W + x; +} + +export function xyOf(i) { + return [i % MAP_W, Math.floor(i / MAP_W)]; +} + +export function inside(x, y) { + return x >= 0 && y >= 0 && x < MAP_W && y < MAP_H; +} + +export function clamp(v, a = 0, b = 1) { + return Math.max(a, Math.min(b, v)); +} + +export function nearMapEdge(x, y, margin = 1) { + return x < margin || y < margin || x >= MAP_W - margin || y >= MAP_H - margin; +} + +export function hash2(x, y, seed) { + let h = Math.imul((x | 0) ^ (seed | 0), 374761393) + Math.imul((y | 0) ^ ((seed >>> 1) | 0), 668265263); + h = (h ^ (h >>> 13)) >>> 0; + h = Math.imul(h, 1274126177) >>> 0; + return ((h ^ (h >>> 16)) >>> 0) / 4294967295; +} + +export function rand(seed, n) { + return hash2(n * 7919 + 17, n * 104729 + 31, seed); +} + +export function smoothstep(t) { + t = clamp(t); + return t * t * (3 - 2 * t); +} + +export function lerp(a, b, t) { + return a + (b - a) * t; +} + +export function valueNoise(x, y, seed, scale) { + const sx = x / scale; + const sy = y / scale; + const x0 = Math.floor(sx); + const y0 = Math.floor(sy); + const tx = smoothstep(sx - x0); + const ty = smoothstep(sy - y0); + + const a = hash2(x0, y0, seed); + const b = hash2(x0 + 1, y0, seed); + const c = hash2(x0, y0 + 1, seed); + const d = hash2(x0 + 1, y0 + 1, seed); + + return lerp(lerp(a, b, tx), lerp(c, d, tx), ty); +} + +export function fbm(x, y, seed) { + let amp = 1; + let scale = 54; + let sum = 0; + let norm = 0; + for (let i = 0; i < 5; i++) { + sum += valueNoise(x, y, seed + i * 101, scale) * amp; + norm += amp; + amp *= 0.5; + scale *= 0.5; + } + return sum / norm; +} + +export function pickEntities(candidates, { max = 99, minDistance = 5, threshold = 0, seed = 0, jitter = 0.04 } = {}) { + const sorted = candidates + .filter((p) => Number.isFinite(p.score) && p.score >= threshold) + .map((p) => ({ ...p, score: p.score + hash2(p.x, p.y, seed + 54321) * jitter })) + .sort((a, b) => b.score - a.score); + + const out = []; + for (const candidate of sorted) { + if (out.every((p) => Math.hypot(p.x - candidate.x, p.y - candidate.y) >= minDistance)) { + out.push(candidate); + if (out.length >= max) break; + } + } + return out; +} + +export function createMapFields() { + const flowTo = new Int32Array(SIZE); + flowTo.fill(-1); + + return { + elevation: new Float32Array(SIZE), + moisture: new Float32Array(SIZE), + slope: new Float32Array(SIZE), + sea: new Uint8Array(SIZE), + river: new Float32Array(SIZE), + floodplain: new Float32Array(SIZE), + plain: new Float32Array(SIZE), + agriculture: new Float32Array(SIZE), + ridgeField: new Float32Array(SIZE), + valleyField: new Float32Array(SIZE), + basinField: new Float32Array(SIZE), + coastalLowland: new Float32Array(SIZE), + flowAccum: new Float32Array(SIZE), + erosionField: new Float32Array(SIZE), + depositionField: new Float32Array(SIZE), + flowTo, + portSuitability: new Float32Array(SIZE), + crossingSuitability: new Float32Array(SIZE), + passSuitability: new Float32Array(SIZE), + }; +} + +export class MinHeap { + constructor() { + this.items = []; + } + + push(item) { + this.items.push(item); + let i = this.items.length - 1; + while (i > 0) { + const parent = (i - 1) >> 1; + if (this.items[parent].f <= item.f) break; + this.items[i] = this.items[parent]; + i = parent; + } + this.items[i] = item; + } + + pop() { + if (this.items.length === 0) return null; + const root = this.items[0]; + const last = this.items.pop(); + if (this.items.length > 0) { + let i = 0; + while (true) { + const left = i * 2 + 1; + const right = left + 1; + if (left >= this.items.length) break; + const child = right < this.items.length && this.items[right].f < this.items[left].f ? right : left; + if (this.items[child].f >= last.f) break; + this.items[i] = this.items[child]; + i = child; + } + this.items[i] = last; + } + return root; + } + + get length() { + return this.items.length; + } +} + +export function weightedScore(terms) { + let total = 0; + for (const [value, weight] of terms) total += value * weight; + return total; +} diff --git a/names.js b/names.js index da7859a..4585f85 100644 --- a/names.js +++ b/names.js @@ -1,16 +1,90 @@ -import { MAP_H, MAP_W, indexOf, inside } from "./grid.js"; -import { hash2 } from "./random.js"; +import { MAP_H, MAP_W, hash2, indexOf, inside } from "./mapUtils.js"; export const NAME_KANJI_POOLS = { - modifiers: [], - inlandTerrain: [], - waterTerrain: [], - coastalTerrain: [], - plants: [], - postfixes: [], - archaicPrefixes: [], - archaicSuffixes: [], - settlementWords: [], + modifiers: [ + "大", "小", "上", "下", "中", + "東", "西", "南", "北", + "新", "古", "本", "元", + "高", "長", "広", "深", "浅", + "白", "黒", "青", "赤", + "奥", "前", "後", "内", "外", + "早", "早", "真", "丸", "平" + ], + + inlandTerrain: [ + "山", "谷", "沢", "原", "野", + "森", "林", "岡", "丘", "坂", + "峰", "峠", "嶺", "尾", "平", + "窪", "久", "洞", "迫", "台", + "塚", "牧", "畑", "田", "森", + "麓", "郷", "里" + ], + + waterTerrain: [ + "川", "河", "江", "瀬", "淵", + "池", "沼", "泉", "井", "湖", + "滝", "渓", "沢", "谷", "津", + "水", "清", "渡", "橋", "堀", + "溝", "湯", "浦", "洲" + ], + + coastalTerrain: [ + "浜", "浦", "津", "崎", "岬", + "島", "磯", "潟", "湊", "港", + "海", "洲", "瀬", "砂", "潮", + "泊", "江", "浦", "灘", "入", + "湾", "戸", "門" + ], + + plants: [ + "松", "杉", "桜", "梅", "栗", + "竹", "楠", "藤", "萩", "葦", + "菅", "榎", "椿", "桐", "柳", + "橘", "柏", "槙", "柿", "桃", + "梨", "桑", "麻", "芦", "茅", + "榊", "楢", "檜", "椎", "柚" + ], + + postfixes: [ + "田", "原", "野", "沢", "谷", + "川", "山", "岡", "森", "林", + "浜", "浦", "津", "崎", "島", + "江", "瀬", "井", "戸", "口", + "辺", "里", "郷", "村", "町", + "宿", "庄", "台", "坂", "橋", + "本", "内", "窪", "平", "塚", + "畑", "牧", "前", "後", "中" + ], + + archaicPrefixes: [ + "伊", "宇", "阿", "安", "佐", + "土", "出", "丹", "播", "但", + "因", "伯", "筑", "肥", "豊", + "日", "紀", "志", "尾", "駿", + "甲", "信", "越", "備", "讃", + "薩", "隠", "美", "三", "若", + "遠", "近", "能", "加", "賀", + "越", "淡", "壱", "対" + ], + + archaicSuffixes: [ + "予", "陀", "芸", "佐", "雲", + "磨", "馬", "幡", "耆", "摩", + "張", "江", "河", "斐", "濃", + "岐", "防", "門", "隅", "向", + "伊", "前", "中", "後", "波", + "勢", "渡", "城", "紫", "野", + "津", "島", "海", "登", "賀", + "良", "美", "智", "智", "代" + ], + + settlementWords: [ + "里", "郷", "村", "町", "宿", + "庄", "院", "宮", "寺", "社", + "城", "館", "屋", "家", "所", + "市", "場", "府", "関", "駅", + "新田", "本郷", "一宮", "国府" + ] }; export const NAME_PROBABILITIES = { diff --git a/qualityMetrics.js b/qualityMetrics.js deleted file mode 100644 index d8dc82d..0000000 --- a/qualityMetrics.js +++ /dev/null @@ -1,141 +0,0 @@ -import { MAP_H, MAP_W, indexOf } from "./grid.js"; - -export 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)); -} - -export 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, - }; -} - -export 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; -} diff --git a/random.js b/random.js deleted file mode 100644 index d657951..0000000 --- a/random.js +++ /dev/null @@ -1,51 +0,0 @@ -import { clamp } from "./grid.js"; - -export function hash2(x, y, seed) { - let h = Math.imul((x | 0) ^ (seed | 0), 374761393) + Math.imul((y | 0) ^ ((seed >>> 1) | 0), 668265263); - h = (h ^ (h >>> 13)) >>> 0; - h = Math.imul(h, 1274126177) >>> 0; - return ((h ^ (h >>> 16)) >>> 0) / 4294967295; -} - -export function rand(seed, n) { - return hash2(n * 7919 + 17, n * 104729 + 31, seed); -} - -export function smoothstep(t) { - t = clamp(t); - return t * t * (3 - 2 * t); -} - -export function lerp(a, b, t) { - return a + (b - a) * t; -} - -export function valueNoise(x, y, seed, scale) { - const sx = x / scale; - const sy = y / scale; - const x0 = Math.floor(sx); - const y0 = Math.floor(sy); - const tx = smoothstep(sx - x0); - const ty = smoothstep(sy - y0); - - const a = hash2(x0, y0, seed); - const b = hash2(x0 + 1, y0, seed); - const c = hash2(x0, y0 + 1, seed); - const d = hash2(x0 + 1, y0 + 1, seed); - - return lerp(lerp(a, b, tx), lerp(c, d, tx), ty); -} - -export function fbm(x, y, seed) { - let amp = 1; - let scale = 54; - let sum = 0; - let norm = 0; - for (let i = 0; i < 5; i++) { - sum += valueNoise(x, y, seed + i * 101, scale) * amp; - norm += amp; - amp *= 0.5; - scale *= 0.5; - } - return sum / norm; -} diff --git a/renderer.js b/renderer.js index d4acb27..ad253bc 100644 --- a/renderer.js +++ b/renderer.js @@ -1,5 +1,4 @@ -import { MAP_W, MAP_H, CELL_SIZE, indexOf } from "./mapGenerator.js"; -import { clamp } from "./grid.js"; +import { CELL_SIZE, MAP_H, MAP_W, clamp, indexOf } from "./mapUtils.js"; function distToNearest(points, x, y, fallback = 999) { let best = fallback; diff --git a/scoring.js b/scoring.js deleted file mode 100644 index b4577c9..0000000 --- a/scoring.js +++ /dev/null @@ -1,5 +0,0 @@ -export function weightedScore(terms) { - let total = 0; - for (const [value, weight] of terms) total += value * weight; - return total; -} diff --git a/test.js b/test.js index 441e172..25a114d 100644 --- a/test.js +++ b/test.js @@ -9,7 +9,6 @@ import { NAME_TEMPLATE_WEIGHTS, generateTemplateName, } from "./names.js"; -import { adminBoundaryMetrics, majorCityCoreIntegrity } from "./qualityMetrics.js"; const result = document.getElementById("result"); const logLines = []; @@ -29,6 +28,146 @@ function assert(condition, message) { } } +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; +} + try { const map = generateMap(12345); const other = generateMap(54321);