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