import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { performance } from 'node:perf_hooks'; import { generateMap } from './mapPipeline.js'; import { createWorldMap } from './worldMap.js'; import { generatePatch } from './mapPatch.js'; import { MAP_H, MAP_W, SIZE, indexOf } from './mapUtils.js'; function hashObject(value) { const hash = createHash('sha256'); const seen = new WeakSet(); function visit(v, path = '') { if (v == null || typeof v !== 'object') { hash.update(`${path}:${typeof v}:${String(v)}\n`); return; } if (ArrayBuffer.isView(v)) { hash.update(`${path}:${v.constructor.name}:${v.length}:`); hash.update(Buffer.from(v.buffer, v.byteOffset, v.byteLength)); return; } if (seen.has(v)) return; seen.add(v); if (Array.isArray(v)) { hash.update(`${path}:array:${v.length}\n`); for (let i = 0; i < v.length; i++) visit(v[i], `${path}[${i}]`); return; } const keys = Object.keys(v).sort(); hash.update(`${path}:object:${keys.join(',')}\n`); for (const key of keys) visit(v[key], `${path}.${key}`); } visit(value); return hash.digest('hex'); } function rasterize(paths) { const occ = new Uint8Array(SIZE); for (const path of paths || []) for (let k = 1; k < (path?.length || 0); k++) { const [x0, y0] = path[k - 1]; const [x1, y1] = path[k]; const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0))); for (let s = 0; s <= steps; s++) { const t = s / steps; const x = Math.round(x0 + (x1 - x0) * t); const y = Math.round(y0 + (y1 - y0) * t); if (x >= 0 && y >= 0 && x < MAP_W && y < MAP_H) occ[indexOf(x, y)] = 1; } } return occ; } function countCells(occ) { let n = 0; for (const v of occ) n += v ? 1 : 0; return n; } function nearPath(paths, point, radius = 0.75) { for (const path of paths || []) for (const [x, y] of path || []) { if (Math.hypot(x - point.x, y - point.y) <= radius) return true; } return false; } function westSelection(world) { const ox = world.originX, oy = world.originY; return { kind: 'lasso', polygon: [ { x: ox - 145, y: oy - 14 }, { x: ox + 8, y: oy - 14 }, { x: ox + 8, y: oy + MAP_H + 14 }, { x: ox - 145, y: oy + MAP_H + 14 }, ] }; } const source = { transport: readFileSync(new URL('./mapTransport.js', import.meta.url), 'utf8'), features: readFileSync(new URL('./mapFeatures.js', import.meta.url), 'utf8'), output: readFileSync(new URL('./mapOutput.js', import.meta.url), 'utf8'), names: readFileSync(new URL('./names.js', import.meta.url), 'utf8'), admin: readFileSync(new URL('./mapAdminStage.js', import.meta.url), 'utf8'), patch: readFileSync(new URL('./mapPatch.js', import.meta.url), 'utf8'), utils: readFileSync(new URL('./mapTransportUtils.js', import.meta.url), 'utf8'), }; for (const retired of ['NAME_PARTS', 'oneKanjiAppendFallbackUsed', 'legacyFallbackUsed', 'changedAfterSnap', 'pendingSeedsUsedForLowlandSplit', 'seedLifecycle', 'seedCellRevivalCount', 'pendingSeedCount']) { assert.equal(Object.values(source).some((text) => text.includes(retired)), false, `retired compatibility/debug symbol remains: ${retired}`); } for (const retired of ['nationalPrune', 'skippedSameComponent', 'finalOutputRoadConnectivity', 'all-road-connect:${pass}', 'Cached candidates']) { assert.equal(Object.values(source).some((text) => text.includes(retired)), false, `retired transport/cache pattern remains: ${retired}`); } assert.equal(source.transport.includes('nearestNetworkPoint'), false, 'unused nearestNetworkPoint remains'); assert.equal(source.utils.includes('occupancyComponentsFromPathGroups'), true, 'shared occupancy component helper missing'); assert.equal(source.utils.includes('rasterizePathCells'), true, 'shared path rasterization helper missing'); assert.equal(source.features.includes('debug.networkConnectivity.added > 0'), true, 'minor final dedupe is not conditional'); const outputStub = source.output.indexOf('const requiredStubsAdded = ensureAdminCenterRoadStubs();'); const outputEndpoint = source.output.indexOf('const endpointConnectorsAdded = connectNearbyRoadEndpoints();'); const outputPrune = source.output.indexOf('const prune = pruneIsolatedFinalRoadComponents();'); assert.ok(outputStub >= 0 && outputEndpoint > outputStub && outputPrune > outputEndpoint, 'output topology mutations must precede final prune'); assert.equal(source.names.includes('export function generateTemplateName'), false, 'test-only generateTemplateName API remains'); assert.equal(source.names.includes('export const NAME_PARTS'), false, 'test-only NAME_PARTS API remains'); assert.equal(source.features.includes('aStarRoutes: 0'), false, 'obsolete constant transport debug remains'); assert.equal(source.features.includes('fieldCorridorTransport: false'), false, 'obsolete constant transport debug remains'); assert.equal(source.features.includes('nationalRoadPopulationCoverage: 0'), false, 'obsolete constant transport debug remains'); const seeds = [1, 3, 5]; const transport = []; let initial = null; for (const seed of seeds) { const t0 = performance.now(); const map = generateMap(seed, { terrainType: 'auto', onProgress() {} }); if (seed === 1) initial = map; const roads = [...(map.minorRoads || []), ...(map.nationalRoads || []), ...(map.externalRoads || []), ...(map.ringRoads || [])]; const roadCells = countCells(rasterize(roads)); const centers = map.adminCenters || map.adminCentersRaw || []; const covered = centers.filter((center) => nearPath(roads, center)).length; const nc = map.transportDebug?.layers?.preAdminRoadFinalization?.networkConnectivity || {}; const output = map.transportDebug?.layers?.finalOutputRoadTopology || {}; assert.equal(covered, centers.length, `seed ${seed}: municipal center lost road access`); assert.ok(roadCells >= 1200, `seed ${seed}: road coverage collapsed (${roadCells})`); assert.ok((nc.attempted || 0) <= 18 * Math.max(1, (nc.rounds || 0)), `seed ${seed}: connectivity candidate loop expanded unexpectedly`); assert.ok((nc.failed || 0) <= (nc.attempted || 0), `seed ${seed}: failed attempt accounting invalid`); if ((nc.added || 0) === 0) assert.ok((nc.rounds || 0) <= 1, `seed ${seed}: no-success connectivity loop repeated rounds`); assert.ok(Number.isFinite(output.components), `seed ${seed}: final output component count missing`); transport.push({ seed, seconds: Math.round((performance.now() - t0) / 10) / 100, roadCells, adminCenters: { total: centers.length, covered }, connectivity: nc, finalOutput: output, }); } const expansionWorld = createWorldMap(structuredClone(initial)); const expStart = performance.now(); const expansion = generatePatch(expansionWorld, westSelection(expansionWorld), { patchMode: 'expansion', terrainType: 'auto', seed: 0x15151515, variant: 0, maxQualityRetries: 1, }); const expansionSeconds = (performance.now() - expStart) / 1000; assert.equal(expansion.ok, true, `expansion failed: ${expansion.code || 'unknown'}`); assert.notEqual(expansion.candidateQuality?.hardPass, false, 'committed failed quality candidate'); assert.equal(expansion.seamDiagnostics?.expansionFootprintEscapedCells || 0, 0, 'footprint write escaped'); assert.equal(expansion.seamDiagnostics?.roadPortalsUnresolved || 0, 0, 'road portal disconnected'); assert.equal(expansion.seamDiagnostics?.railPortalsUnresolved || 0, 0, 'rail portal disconnected'); const rollbackWorld = createWorldMap(structuredClone(initial)); const before = { fields: hashObject(rollbackWorld.fields), sourceMap: hashObject(rollbackWorld.sourceMap), generatedRects: structuredClone(rollbackWorld.generatedRects), invalidatedRects: structuredClone(rollbackWorld.invalidatedRects), serial: rollbackWorld.patchGenerationSerial || 0, sourceIdentity: rollbackWorld.sourceMap, }; const rejected = generatePatch(rollbackWorld, { x0: 30, y0: 30, x1: 110, y1: 110 }, { patchMode: 'expansion', terrainType: 'oceanic_archipelago', seed: 456, variant: 0, maxQualityRetries: 0, }); assert.equal(rejected.ok, false, 'rollback probe unexpectedly committed'); assert.equal(rejected.code, 'patch-quality-gate-failed'); assert.equal(hashObject(rollbackWorld.fields), before.fields, 'field rollback mismatch'); assert.equal(hashObject(rollbackWorld.sourceMap), before.sourceMap, 'sourceMap rollback mismatch'); assert.deepEqual(rollbackWorld.generatedRects, before.generatedRects, 'generatedRects rollback mismatch'); assert.deepEqual(rollbackWorld.invalidatedRects, before.invalidatedRects, 'invalidatedRects rollback mismatch'); assert.equal(rollbackWorld.patchGenerationSerial || 0, before.serial, 'serial rollback mismatch'); assert.equal(rollbackWorld.sourceMap, before.sourceIdentity, 'sourceMap identity rollback mismatch'); console.log(JSON.stringify({ ok: true, transport, expansion: { seconds: Math.round(expansionSeconds * 100) / 100, seamStatus: expansion.seamDiagnostics?.status || null, footprintEscapedCells: expansion.seamDiagnostics?.expansionFootprintEscapedCells || 0, roadPortalsUnresolved: expansion.seamDiagnostics?.roadPortalsUnresolved || 0, railPortalsUnresolved: expansion.seamDiagnostics?.railPortalsUnresolved || 0, }, rollback: { code: rejected.code, restored: true, sourceIdentityPreserved: true }, }, null, 2));