import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { generateMap } from './mapGenerator.js'; import { createWorldMap } from './worldMap.js'; import { generatePatch } from './mapPatch.js'; import { MAP_W, MAP_H, worldIndexOf } from './mapUtils.js'; const appSource = readFileSync(new URL('./app.js', import.meta.url), 'utf8'); const htmlSource = readFileSync(new URL('./index.html', import.meta.url), 'utf8'); assert.match(htmlSource, /id="clearPatchSelection"/); assert.match(appSource, /clearPatchSelectionButton\?\.addEventListener\("click", \(\) => hideSelectionOverlay\(\{ discardPreview: true \}\)\)/); assert.match(appSource, /function cancelPatchGeneration/); assert.match(appSource, /PATCH_WORKER_INACTIVITY_WATCHDOG_MS/); const initial = generateMap(24681357, { terrainType: 'auto', onProgress() {} }); const world = createWorldMap(structuredClone(initial)); const establishedEdgeX = world.originX + MAP_W - 1; const y0 = world.originY + Math.floor(MAP_H * 0.22); const y1 = world.originY + Math.floor(MAP_H * 0.78); const rect = { x0: establishedEdgeX - 33, y0, x1: establishedEdgeX + 93, y1, }; const result = generatePatch(world, rect, { patchMode: 'expansion', terrainType: 'auto', seed: 97531, variant: 1, maxQualityRetries: 0, qualityTerrainAttempts: 1, acceptBestAvailableQuality: true, }); assert.equal(result.ok, true, result.reason || result.code || 'patch failed'); const source = world.sourceMap; const sea = world.fields.sea; let riverSeaPoints = 0; const writeRect = result.rects?.writeRect || rect; for (const key of ['mainRivers','tributaryRivers','smallStreams','riverPaths']) { for (const path of source[key] || []) for (const t of path || []) { const x = Math.round((t?.[0] || 0) + world.originX); const y = Math.round((t?.[1] || 0) + world.originY); const i = worldIndexOf(world,x,y); if (i >= 0 && sea[i] && x >= writeRect.x0 && x < writeRect.x1 && y >= writeRect.y0 && y < writeRect.y1) riverSeaPoints++; } } assert.equal(riverSeaPoints, 0, `river path points over sea inside patch writeRect: ${riverSeaPoints}`); const byPref = new Map(); for (const city of source.modernCities || []) { const capital = city.isPrefecturalCapital || city.isRegionalCapital || /Capital/i.test(String(city.rank || '')) || /Capital/i.test(String(city.kind || '')); if (!capital) continue; const x = Math.round((city.x || 0) + world.originX); const y = Math.round((city.y || 0) + world.originY); const i = worldIndexOf(world,x,y); if (i < 0 || sea[i] || world.fields.prefectureRegionId[i] < 0) continue; const id = world.fields.prefectureRegionId[i]; byPref.set(id, (byPref.get(id) || 0) + 1); } const duplicateCapitalPrefs = [...byPref.entries()].filter(([,n]) => n > 1); assert.equal(duplicateCapitalPrefs.length, 0, `duplicate capital prefectures: ${JSON.stringify(duplicateCapitalPrefs)}`); const activePrefIds = new Set(); for (let i=0;i= 0) activePrefIds.add(Math.floor(world.fields.prefectureRegionId[i])); const missingCapitalPrefs = [...activePrefIds].filter((id) => (byPref.get(id) || 0) === 0); assert.equal(missingCapitalPrefs.length, 0, `missing capital prefectures: ${JSON.stringify(missingCapitalPrefs)}`); const prefCounts = new Map(); for (let i=0;i= 0) { const id=world.fields.prefectureRegionId[i]; prefCounts.set(id,(prefCounts.get(id)||0)+1); } const generatedRegions = (source.prefectureRegions || []).filter(p => p.patchGenerated).map(p => p.prefectureRegionId ?? p.id).filter(Number.isFinite); const generatedSizes = [...new Set(generatedRegions)].map(id => [id,prefCounts.get(id)||0]); const tinyGenerated = generatedSizes.filter(([,n]) => n > 0 && n < 650); assert.equal(tinyGenerated.length, 0, `tiny generated prefectures survived: ${JSON.stringify(tinyGenerated)}`); let landLandPairs = 0; let maxEstablishedFrontierElevationJump = 0; let adminBreaksOnEstablishedFrontier = 0; let prefectureBreaksOnEstablishedFrontier = 0; for (let y=y0; y 20, 'frontier probe did not include enough land pairs'); assert.ok(maxEstablishedFrontierElevationJump < 0.08, `frontier elevation jump too large: ${maxEstablishedFrontierElevationJump}`); assert.equal(adminBreaksOnEstablishedFrontier, 0, 'municipal boundary still follows generation frontier'); assert.equal(prefectureBreaksOnEstablishedFrontier, 0, 'prefecture boundary still follows generation frontier'); assert.equal(result.seamDiagnostics?.hardPass, true, `seam gate failed: ${JSON.stringify(result.seamDiagnostics?.gateReasons || [])}`); console.log(JSON.stringify({ ok: true, riverSeaPoints, duplicateCapitalPrefs, missingCapitalPrefs, generatedSizes, tinyGenerated, landLandPairs, maxEstablishedFrontierElevationJump: Math.round(maxEstablishedFrontierElevationJump * 10000) / 10000, adminBreaksOnEstablishedFrontier, prefectureBreaksOnEstablishedFrontier, frontierAdminCellsAligned: result.humanGeography?.frontierAdminCellsAligned || 0, establishedFrontierAdminCellsRestored: result.humanGeography?.establishedFrontierAdminCellsRestored || 0, frontierHarmonizedValues: result.humanGeography?.frontierHarmonizedValues || 0, seamStatus: result.seamDiagnostics?.status, seamGateReasons: result.seamDiagnostics?.gateReasons || [], }, null, 2));