map/archive/generated-history/STEP4_STEP5_VALIDATION.mjs
2026-08-08 17:41:30 +09:00

181 lines
8.3 KiB
JavaScript

import assert from "node:assert/strict";
import { generateMap } from "./mapPipeline.js";
import { MAP_H } from "./mapUtils.js";
import { generatePatch } from "./mapPatch.js";
import { createWorldMap } from "./worldMap.js";
function stage(patch, key) {
return patch?.humanGeography?.patchStages?.[key];
}
function segmentKey(segment) {
const a = `${segment[0][0]},${segment[0][1]}`;
const b = `${segment[1][0]},${segment[1][1]}`;
return a < b ? `${a}|${b}` : `${b}|${a}`;
}
function validateUniqueSegments(segments, label) {
const seen = new Set();
for (const segment of segments || []) {
const key = segmentKey(segment);
assert(!seen.has(key), `${label} contains a duplicate segment ${key}`);
seen.add(key);
}
return seen;
}
function adjacentCellsForSegment(world, segment) {
const ax = segment[0][0] + world.originX;
const ay = segment[0][1] + world.originY;
const bx = segment[1][0] + world.originX;
const by = segment[1][1] + world.originY;
if (Math.abs(ax - bx) < 1e-6) {
const lineX = Math.round(ax);
const y = Math.floor((ay + by) * 0.5);
return [[lineX - 1, y], [lineX, y]];
}
if (Math.abs(ay - by) < 1e-6) {
const x = Math.floor((ax + bx) * 0.5);
const lineY = Math.round(ay);
return [[x, lineY - 1], [x, lineY]];
}
throw new Error(`non-axis-aligned raster boundary ${JSON.stringify(segment)}`);
}
function worldIndex(world, x, y) {
assert(x >= 0 && y >= 0 && x < world.width && y < world.height, `boundary cell out of world: ${x},${y}`);
return y * world.width + x;
}
function validateFinalIdBoundaries(world) {
const sourceMap = world.sourceMap;
const admin = world.fields.adminId;
const pref = world.fields.prefectureRegionId;
const sea = world.fields.sea;
const mask = world.fields.prefectureMask;
const adminKeys = validateUniqueSegments(sourceMap.adminBorders, "adminBorders");
const prefKeys = validateUniqueSegments(sourceMap.regionalPrefectureBorders, "regionalPrefectureBorders");
validateUniqueSegments(sourceMap.prefectureBorder, "prefectureBorder");
for (const segment of sourceMap.adminBorders || []) {
const [[ax, ay], [bx, by]] = adjacentCellsForSegment(world, segment);
const ai = worldIndex(world, ax, ay);
const bi = worldIndex(world, bx, by);
assert(!sea[ai] && !sea[bi], "municipal border must separate two land cells");
assert(mask[ai] && mask[bi], "municipal border must remain inside the final prefecture mask");
assert(admin[ai] >= 0 && admin[bi] >= 0 && admin[ai] !== admin[bi], "municipal border must match differing final admin IDs");
assert(pref[ai] >= 0 && pref[ai] === pref[bi], "municipal border must not duplicate a prefecture border");
}
for (const segment of sourceMap.regionalPrefectureBorders || []) {
const [[ax, ay], [bx, by]] = adjacentCellsForSegment(world, segment);
const ai = worldIndex(world, ax, ay);
const bi = worldIndex(world, bx, by);
assert(!sea[ai] && !sea[bi], "prefecture border must separate two land cells");
assert(mask[ai] && mask[bi], "prefecture border must remain inside the final prefecture mask");
assert(pref[ai] >= 0 && pref[bi] >= 0 && pref[ai] !== pref[bi], "prefecture border must match differing final prefecture IDs");
}
for (const key of adminKeys) assert(!prefKeys.has(key), `municipal and prefecture layers overlap at ${key}`);
for (const segment of sourceMap.prefectureBorder || []) {
const [[ax, ay], [bx, by]] = adjacentCellsForSegment(world, segment);
const ai = worldIndex(world, ax, ay);
const bi = worldIndex(world, bx, by);
assert(!sea[ai] && !sea[bi], "outer prefecture mask border must not be drawn through water");
assert(Boolean(mask[ai]) !== Boolean(mask[bi]), "outer prefecture border must match the final mask edge");
}
return {
adminSegments: sourceMap.adminBorders?.length || 0,
prefectureSegments: sourceMap.regionalPrefectureBorders?.length || 0,
outerMaskSegments: sourceMap.prefectureBorder?.length || 0,
};
}
const initial = generateMap(114514, { terrainType: "setouchi_inland_sea", onProgress() {} });
const expansionWorld = createWorldMap(structuredClone(initial));
const ox = expansionWorld.originX;
const oy = expansionWorld.originY;
const expansionSelection = {
kind: "lasso",
polygon: [
{ x: ox - 92, y: oy + 28 },
{ x: ox + 34, y: oy + 28 },
{ x: ox + 34, y: oy + MAP_H - 28 },
{ x: ox - 92, y: oy + MAP_H - 28 },
],
};
const expansionPatch = generatePatch(expansionWorld, expansionSelection, {
patchMode: "auto",
terrainType: "setouchi_inland_sea",
seed: 0x1234abcd,
variant: 0,
});
assert.equal(expansionPatch.ok, true);
assert.equal(expansionPatch.patchMode, "expansion");
const expansionTransport = stage(expansionPatch, "transport");
const expansionSegments = stage(expansionPatch, "segments");
assert(expansionTransport.roadPortalsRequired > 0, "expansion fixture must contain a mandatory road portal");
assert.equal(expansionTransport.roadPortalsUnresolved, 0, "every expansion road portal must reconnect");
assert.equal(expansionPatch.seamDiagnostics.roadPortalsBroken, 0, "rendered expansion road portals must be connected");
assert.equal(expansionPatch.seamDiagnostics.railPortalsBroken, 0, "rendered expansion rail portals must be connected");
assert.equal(expansionSegments.boundarySource, "final-id-rasters");
assert.equal(expansionSegments.globalBoundaryRebuild, true);
assert.equal(expansionSegments.candidateAdminBordersMerged, 0);
assert.equal(expansionSegments.candidatePrefectureBordersMerged, 0);
assert.equal(expansionPatch.seamDiagnostics.duplicateBoundaryPairs, 0, "final-raster boundary rebuild must not leave seam twins");
const expansionBoundaryCounts = validateFinalIdBoundaries(expansionWorld);
const regenerationWorld = createWorldMap(structuredClone(initial));
const regenerationSelection = {
x0: regenerationWorld.originX + 70,
y0: regenerationWorld.originY + 40,
x1: regenerationWorld.originX + 190,
y1: regenerationWorld.originY + 150,
};
const regenerationPatch = generatePatch(regenerationWorld, regenerationSelection, {
patchMode: "regeneration",
terrainType: "setouchi_inland_sea",
seed: 0x7412abce,
variant: 1,
});
assert.equal(regenerationPatch.ok, true);
assert.equal(regenerationPatch.patchMode, "regeneration");
const regenerationTransport = stage(regenerationPatch, "transport");
const regenerationSegments = stage(regenerationPatch, "segments");
assert(regenerationTransport.roadPortalsRequired > 0, "regeneration fixture must contain road portals");
assert(regenerationTransport.railPortalsRequired > 0, "regeneration fixture must contain a rail portal");
assert.equal(regenerationTransport.roadPortalsUnresolved, 0, "every regeneration road portal must reconnect");
assert.equal(regenerationTransport.railPortalsUnresolved, 0, "every regeneration rail portal must reconnect");
assert.equal(regenerationPatch.seamDiagnostics.roadPortalsBroken, 0);
assert.equal(regenerationPatch.seamDiagnostics.railPortalsBroken, 0);
assert.equal(regenerationPatch.seamDiagnostics.duplicateBoundaryPairs, 0);
assert.equal(regenerationSegments.boundarySource, "final-id-rasters");
assert.equal(regenerationSegments.candidateAdminBordersMerged, 0);
assert.equal(regenerationSegments.candidatePrefectureBordersMerged, 0);
const regenerationBoundaryCounts = validateFinalIdBoundaries(regenerationWorld);
console.log(JSON.stringify({
ok: true,
expansion: {
roadPortals: expansionTransport.roadPortalsRequired,
roadPortalConnectors: expansionTransport.roadPortalConnectorsAdded,
railPortals: expansionTransport.railPortalsRequired,
brokenRoadPortals: expansionPatch.seamDiagnostics.roadPortalsBroken,
brokenRailPortals: expansionPatch.seamDiagnostics.railPortalsBroken,
duplicateBoundaryPairs: expansionPatch.seamDiagnostics.duplicateBoundaryPairs,
boundaries: expansionBoundaryCounts,
},
regeneration: {
roadPortals: regenerationTransport.roadPortalsRequired,
roadPortalConnectors: regenerationTransport.roadPortalConnectorsAdded,
railPortals: regenerationTransport.railPortalsRequired,
railPortalConnectors: regenerationTransport.railPortalConnectorsAdded,
brokenRoadPortals: regenerationPatch.seamDiagnostics.roadPortalsBroken,
brokenRailPortals: regenerationPatch.seamDiagnostics.railPortalsBroken,
duplicateBoundaryPairs: regenerationPatch.seamDiagnostics.duplicateBoundaryPairs,
boundaries: regenerationBoundaryCounts,
},
}, null, 2));