From f420a3f8f3e84cc78716e813dbd727298b8603c8 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Thu, 21 May 2026 03:13:39 +0900 Subject: [PATCH] tweak --- adminRegions.js | 4 +- entitySelection.js | 17 ----- fields.js | 28 -------- graph.js | 34 --------- grid.js | 26 ------- mapGenerator.js | 8 +-- mapUtils.js | 167 +++++++++++++++++++++++++++++++++++++++++++++ names.js | 96 +++++++++++++++++++++++--- qualityMetrics.js | 141 -------------------------------------- random.js | 51 -------------- renderer.js | 3 +- scoring.js | 5 -- test.js | 141 +++++++++++++++++++++++++++++++++++++- 13 files changed, 396 insertions(+), 325 deletions(-) delete mode 100644 entitySelection.js delete mode 100644 fields.js delete mode 100644 graph.js delete mode 100644 grid.js create mode 100644 mapUtils.js delete mode 100644 qualityMetrics.js delete mode 100644 random.js delete mode 100644 scoring.js 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);