147 lines
7 KiB
JavaScript
147 lines
7 KiB
JavaScript
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 crest = map.crestCrossingScore?.[i] || 0;
|
|
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 + crest * 1.38 + majorRiver * 0.88 + minorStream * 0.22 + slopeBreak + highGround + valleyFloorPenalty - urbanPenalty * 0.72));
|
|
}
|
|
|
|
export function adminBoundaryMetrics(map) {
|
|
let borderEdges = 0;
|
|
let targetSum = 0;
|
|
let crestSum = 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;
|
|
const crestSupport = ((map.crestCrossingScore?.[i] || 0) + (map.crestCrossingScore?.[ni] || 0)) * 0.5;
|
|
targetSum += edgeTarget;
|
|
crestSum += crestSupport;
|
|
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);
|
|
const naturalSupport = Math.max(edgeTarget, crestSupport, map.ridgeField[i], map.ridgeField[ni], map.river[i], map.river[ni]);
|
|
if (Math.abs(dA - dB) < 4.2 && naturalSupport < 0.42) 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,
|
|
avgCrest: borderEdges ? crestSum / 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;
|
|
}
|