not good but not bad

This commit is contained in:
33333-33333 2026-05-26 15:32:27 +09:00
commit 4f0df3f6c5
11 changed files with 1284 additions and 622 deletions

View file

@ -474,20 +474,21 @@ export function generateRegionalPrefectures(seed, sea, elevation, slope, river,
}
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);
const displayRegionId = new Int16Array(beforeRegionId);
for (let pass = 0; pass < 3; pass++) repairRegionalTopology(displayRegionId, sea, seeded.centers, anchorMask, 200);
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);
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);
const afterNaturalAverage = Math.max(measuredAfterNaturalAverage, beforeNaturalAverage);
const afterNaturalAverage = measuredAfterNaturalAverage;
const afterVoronoiLikeRate = regionalVoronoiLikeRate(regionId, seeded.centers, sea, naturalBarrierScore);
return {
regionId,
displayRegionId,
centers: seeded.centers,
naturalBarrierScore,
debug: {
@ -498,13 +499,16 @@ export function generateRegionalPrefectures(seed, sea, elevation, slope, river,
regionalVoronoiLikeRateAfter: afterVoronoiLikeRate,
regionalNaturalBarrierAverageBefore: beforeNaturalAverage,
regionalNaturalBarrierAverageAfter: afterNaturalAverage,
regionalDisplayBorderCount: countRegionBorderEdges(displayRegionId, sea),
regionalDisplayNaturalBarrierAverage: averageRegionBorderBarrier(displayRegionId, sea, naturalBarrierScore),
regionalDisplayBorderCount: countRegionBorderEdges(regionId, sea),
regionalDisplayNaturalBarrierAverage: averageRegionBorderBarrier(regionId, sea, naturalBarrierScore),
regionalCompartmentCount: compartments.filter((unit) => unit.area > 0).length,
compartmentCount: compartments.filter((unit) => unit.area > 0).length,
changedAfterCompartmentAssignment: changed,
borderNaturalBarrierAverage: afterNaturalAverage,
voronoiLikeRate: afterVoronoiLikeRate,
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),
},
};
}
@ -537,9 +541,9 @@ function generateRegionalPrefecturesSeedGrowth(seed, sea, elevation, slope, rive
}
}
centers.push(...pickEntities(candidates, {
max: 9 + Math.floor(rand(seed, 6101) * 6),
minDistance: 22,
threshold: 0.38,
max: 6 + Math.floor(rand(seed, 6101) * 4),
minDistance: 31,
threshold: 0.42,
seed: seed + 6102,
jitter: 0.02,
}));
@ -583,6 +587,357 @@ function generateRegionalPrefecturesSeedGrowth(seed, sea, elevation, slope, rive
return { regionId, centers };
}
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;
}
}
function buildRegionalNaturalBarrierScore(sea, elevation, slope, river, ridgeField, flowAccum) {
const score = new Float32Array(SIZE);
for (let y = 0; y < MAP_H; y++) {
@ -811,20 +1166,50 @@ function repairRegionalTopology(regionId, sea, centers, anchorMask, maxIslandCel
for (let i = 0; i < SIZE; i++) if (anchorMask[i] && !sea[i]) regionId[i] = 0;
}
export function extractRegionBorderSegments(regionId, sea) {
export function extractRegionBorderSegments(regionId, sea, options = {}) {
const segments = [];
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);
};
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)]) {
const b = regionId[indexOf(x + 1, y)];
const ni = indexOf(x + 1, y);
const b = regionId[ni];
if (b >= 0 && a !== b) segments.push([[x + 1, y], [x + 1, y + 1]]);
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) || "-"}`);
}
if (y + 1 < MAP_H && !sea[indexOf(x, y + 1)]) {
const b = regionId[indexOf(x, y + 1)];
const ni = indexOf(x, y + 1);
const b = regionId[ni];
if (b >= 0 && a !== b) segments.push([[x, y + 1], [x + 1, y + 1]]);
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) || "-"}`);
}
}
}