This commit is contained in:
33333-33333 2026-05-21 03:13:39 +09:00
commit f420a3f8f3
13 changed files with 396 additions and 325 deletions

View file

@ -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 = [];

View file

@ -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;
}

View file

@ -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),
};
}

View file

@ -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; }
}

26
grid.js
View file

@ -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;
}

View file

@ -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 = [];

167
mapUtils.js Normal file
View file

@ -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;
}

View file

@ -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 = {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -1,5 +0,0 @@
export function weightedScore(terms) {
let total = 0;
for (const [value, weight] of terms) total += value * weight;
return total;
}

141
test.js
View file

@ -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);