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

109 lines
5.9 KiB
JavaScript

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<world.fields.prefectureRegionId.length;i++) if (!sea[i] && world.fields.prefectureRegionId[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<world.fields.prefectureRegionId.length;i++) if (!sea[i] && world.fields.prefectureRegionId[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<y1; y++) {
const a = worldIndexOf(world, establishedEdgeX, y);
const b = worldIndexOf(world, establishedEdgeX + 1, y);
if (a < 0 || b < 0 || sea[a] || sea[b]) continue;
landLandPairs++;
maxEstablishedFrontierElevationJump = Math.max(maxEstablishedFrontierElevationJump, Math.abs(world.fields.elevation[a] - world.fields.elevation[b]));
if (world.fields.adminId[a] !== world.fields.adminId[b]) adminBreaksOnEstablishedFrontier++;
if (world.fields.prefectureRegionId[a] !== world.fields.prefectureRegionId[b]) prefectureBreaksOnEstablishedFrontier++;
}
assert.ok(landLandPairs > 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));