181 lines
8.3 KiB
JavaScript
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));
|