q
This commit is contained in:
parent
810ad6f5cb
commit
de7c6c32bd
123 changed files with 13201 additions and 6213 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { generateMap } from "../src/mapPipeline.js";
|
||||
|
|
@ -9,15 +10,16 @@ const baseline = createWorldMap(initial);
|
|||
const rect = { x0: 20, y0: 120, x1: 180, y1: 230 };
|
||||
const terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
|
||||
|
||||
function runWorkerCase(patchMode, id) {
|
||||
function runWorkerCase(patchMode, id, overrides = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
|
||||
const timer = setTimeout(async () => {
|
||||
try { await worker.terminate(); } catch {}
|
||||
reject(new Error(`${patchMode} coverage regression timed out`));
|
||||
}, 60_000);
|
||||
}, overrides.timeoutMs ?? 90_000);
|
||||
const startedAt = performance.now();
|
||||
const seed = 123;
|
||||
const seed = overrides.seed ?? 123;
|
||||
const candidatePlan = overrides.candidatePlan || [{ candidateId: `coverage-${patchMode}:0`, candidateOrdinal: 1, variant: 0, seed }];
|
||||
worker.on("error", reject);
|
||||
worker.on("message", async (message) => {
|
||||
if (message.id !== id || message.type === "progress") return;
|
||||
|
|
@ -35,7 +37,7 @@ function runWorkerCase(patchMode, id) {
|
|||
variant: 0,
|
||||
seed,
|
||||
maxQualityRetries: 0,
|
||||
qualityTerrainAttempts: 1,
|
||||
qualityTerrainAttempts: candidatePlan.length,
|
||||
acceptBestAvailableQuality: false,
|
||||
includeSeamVisualization: false,
|
||||
},
|
||||
|
|
@ -45,16 +47,46 @@ function runWorkerCase(patchMode, id) {
|
|||
committedRevision: 1,
|
||||
workerEpoch: 1,
|
||||
executionAttempt: 1,
|
||||
totalCandidateCount: 1,
|
||||
candidatePlan: [{ candidateId: `coverage-${patchMode}:0`, candidateOrdinal: 1, variant: 0, seed }],
|
||||
totalCandidateCount: candidatePlan.length,
|
||||
candidatePlan,
|
||||
...(overrides.search || {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const bestOfPlan = [0, 1, 2].map((variant, index) => ({
|
||||
candidateId: `coverage-bestof:${variant}`,
|
||||
candidateOrdinal: index + 1,
|
||||
variant,
|
||||
seed: (123 + variant) >>> 0,
|
||||
}));
|
||||
const { message: bestOfMessage } = await runWorkerCase("expansion", 3, {
|
||||
candidatePlan: bestOfPlan,
|
||||
search: {
|
||||
draftSelection: true,
|
||||
selectBestCandidate: true,
|
||||
parallelDrafts: false,
|
||||
resolvedPatchMode: "expansion",
|
||||
},
|
||||
});
|
||||
assert.equal(bestOfMessage.ok, true, bestOfMessage.error || bestOfMessage.code || "best-of worker failed");
|
||||
const bestOfResult = bestOfMessage.result || {};
|
||||
assert.equal(bestOfResult.ok, true, bestOfResult.reason || bestOfResult.code || "best-of candidate search failed");
|
||||
assert.equal(bestOfResult.searchStatus, "succeeded");
|
||||
assert.equal(bestOfResult.bestOfCandidates, true, "best-of selection remains enabled");
|
||||
assert.equal(bestOfResult.draftSelection?.enabled, true, "Branch-and-Bound ranking remains enabled");
|
||||
assert.equal(Number(bestOfResult.candidateUnmappedActiveCells || 0), 0, "best candidate covers every active write cell");
|
||||
assert.ok((bestOfResult.searchAttempts || []).length >= bestOfPlan.length, "every planned candidate remains visible to selection");
|
||||
assert.equal((bestOfResult.searchAttempts || []).some((attempt) => attempt.code === "candidate-execution-error" && /did not cover/i.test(attempt.reason || "")), false,
|
||||
"parent operation context never leaks into the canonical internal tile");
|
||||
|
||||
for (const [index, patchMode] of ["auto", "expansion"].entries()) {
|
||||
const { message, elapsedMs } = await runWorkerCase(patchMode, index + 1);
|
||||
const result = message.result || {};
|
||||
const topology = result.candidateQuality?.finalMerge?.transportTopology;
|
||||
const prefectureCoherence = result.candidateQuality?.finalMerge?.prefectureCoherence;
|
||||
const demandedTopologyConnected = Object.values(topology?.byClass || {}).every((entry) => entry?.hardPass !== false);
|
||||
const ok = message.ok === true
|
||||
&& result.ok === true
|
||||
&& result.searchStatus === "succeeded"
|
||||
|
|
@ -62,7 +94,10 @@ for (const [index, patchMode] of ["auto", "expansion"].entries()) {
|
|||
&& result.tiledExpansion === true
|
||||
&& Number(result.tileCount) === 1
|
||||
&& Number(result.candidateUnmappedActiveCells || 0) === 0
|
||||
&& result.seamDiagnostics?.hardPass === true;
|
||||
&& result.seamDiagnostics?.hardPass === true
|
||||
&& topology?.hardPass === true
|
||||
&& demandedTopologyConnected
|
||||
&& prefectureCoherence?.hardPass === true;
|
||||
console.log(JSON.stringify({
|
||||
patchMode,
|
||||
ok,
|
||||
|
|
@ -75,6 +110,9 @@ for (const [index, patchMode] of ["auto", "expansion"].entries()) {
|
|||
tileCount: Number(result.tileCount || 0),
|
||||
candidateUnmappedActiveCells: Number(result.candidateUnmappedActiveCells || 0),
|
||||
seamPass: result.seamDiagnostics?.hardPass === true,
|
||||
transportTopologyPass: topology?.hardPass === true,
|
||||
transportTopology: topology?.byClass || null,
|
||||
prefectureCoherencePass: prefectureCoherence?.hardPass === true,
|
||||
reason: result.reason || message.error || null,
|
||||
}, null, 2));
|
||||
if (!ok) process.exitCode = 1;
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Additional generation browser E2E</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 24px; background: #111827; color: #e5e7eb; font: 14px/1.5 ui-monospace, monospace; }
|
||||
pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 18px; border-radius: 10px; background: #030712; }
|
||||
[data-status="pass"] pre { border: 2px solid #22c55e; }
|
||||
[data-status="fail"] pre { border: 2px solid #ef4444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Additional generation browser E2E</h1>
|
||||
<pre id="result">RUNNING</pre>
|
||||
<script type="module" src="./additional-generation-e2e.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
import { createWorldMap } from "../src/worldMap.js";
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const resultEl = document.getElementById("result");
|
||||
const seed = (Number(params.get("seed")) || 114514) >>> 0;
|
||||
const startVariant = (Number(params.get("variant")) || 0) >>> 0;
|
||||
const candidateLimit = Math.max(1, Math.min(3, Number(params.get("candidates")) || 2));
|
||||
const selectionWidth = Math.max(48, Math.floor(Number(params.get("width")) || 60));
|
||||
const selectionHeight = Math.max(48, Math.floor(Number(params.get("height")) || 60));
|
||||
const patchBudgetMs = Math.max(1000, Number(params.get("budgetMs")) || 60000);
|
||||
|
||||
function deriveSeed(worldSeed, terrainType, variant) {
|
||||
let h = (worldSeed >>> 0) ^ 0x9e3779b9;
|
||||
h = Math.imul(h ^ (variant >>> 0), 668265263) >>> 0;
|
||||
for (const ch of String(terrainType || "auto")) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0;
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
function waitForGeneration(worker) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = 1;
|
||||
const progress = [];
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.id !== id) return;
|
||||
if (event.data.type === "progress") {
|
||||
progress.push({ at: performance.now(), ...(event.data.progress || event.data.event || {}) });
|
||||
return;
|
||||
}
|
||||
worker.removeEventListener("message", onMessage);
|
||||
if (event.data.ok) resolve({ map: event.data.map, progress });
|
||||
else reject(new Error(event.data.error || "Initial generation failed"));
|
||||
};
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.addEventListener("error", (event) => reject(new Error(event.message || "Initial generation Worker crashed")), { once: true });
|
||||
worker.postMessage({ id, seed, options: { terrainType: params.get("terrain") || "auto" } });
|
||||
});
|
||||
}
|
||||
|
||||
function waitForApplyAck(worker, patch) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const applyToken = patch.result?.applyToken;
|
||||
if (!applyToken) {
|
||||
reject(new Error("Accepted patch did not provide a transactional Apply token."));
|
||||
return;
|
||||
}
|
||||
const ackId = `e2e-apply-${Date.now()}`;
|
||||
const timer = setTimeout(() => reject(new Error("Transactional Apply ACK timed out.")), 30000);
|
||||
const onMessage = (event) => {
|
||||
const data = event.data || {};
|
||||
if (data.type !== "patch-apply-ack-result" || data.ackId !== ackId) return;
|
||||
clearTimeout(timer);
|
||||
worker.removeEventListener("message", onMessage);
|
||||
if (data.ok) resolve(data);
|
||||
else reject(new Error(data.error || "Transactional Apply ACK failed."));
|
||||
};
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.postMessage({
|
||||
type: "patch-apply-ack",
|
||||
ackId,
|
||||
applyToken,
|
||||
baseCommittedRevision: 1,
|
||||
committedRevision: 2,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function heapSnapshot(label) {
|
||||
return performance.memory ? {
|
||||
label,
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
||||
} : null;
|
||||
}
|
||||
|
||||
function waitForPatch(worker, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const progress = [];
|
||||
const startedAt = performance.now();
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.id !== message.id) return;
|
||||
if (event.data.type === "progress") {
|
||||
progress.push({ at: performance.now(), ...event.data.progress });
|
||||
return;
|
||||
}
|
||||
worker.removeEventListener("message", onMessage);
|
||||
if (event.data.ok) resolve({ ...event.data, progress, wallMs: performance.now() - startedAt });
|
||||
else reject(new Error(event.data.error || "Patch Worker failed"));
|
||||
};
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.addEventListener("messageerror", () => reject(new Error("Patch result could not be deserialized")), { once: true });
|
||||
worker.addEventListener("error", (event) => reject(new Error(event.message || "Patch Worker crashed")), { once: true });
|
||||
worker.postMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
function maxProgressGap(progress, start, end) {
|
||||
const times = [start, ...(progress || []).map((entry) => entry.at), end];
|
||||
let max = 0;
|
||||
for (let index = 1; index < times.length; index++) max = Math.max(max, times[index] - times[index - 1]);
|
||||
return max;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const heap = [heapSnapshot("start")].filter(Boolean);
|
||||
const initialWorker = new Worker(new URL("../src/generationWorker.js", import.meta.url), { type: "module" });
|
||||
const initialStartedAt = performance.now();
|
||||
const initial = await waitForGeneration(initialWorker);
|
||||
const afterInitialHeap = heapSnapshot("after-initial");
|
||||
if (afterInitialHeap) heap.push(afterInitialHeap);
|
||||
const initialEndedAt = performance.now();
|
||||
initialWorker.terminate();
|
||||
const world = createWorldMap(initial.map);
|
||||
const rect = {
|
||||
x0: world.originX + Math.max(0, Math.floor((258 - selectionWidth) / 2)),
|
||||
y0: world.originY + Math.max(0, Math.floor((183 - selectionHeight) / 2)),
|
||||
x1: world.originX + Math.max(0, Math.floor((258 - selectionWidth) / 2)) + selectionWidth,
|
||||
y1: world.originY + Math.max(0, Math.floor((183 - selectionHeight) / 2)) + selectionHeight,
|
||||
};
|
||||
if (params.get("shape") === "lasso") {
|
||||
const insetX = Math.max(4, Math.floor(selectionWidth * 0.16));
|
||||
const insetY = Math.max(4, Math.floor(selectionHeight * 0.16));
|
||||
rect.kind = "lasso";
|
||||
rect.polygon = [
|
||||
{ x: rect.x0 + insetX, y: rect.y0 },
|
||||
{ x: rect.x1 - 1, y: rect.y0 + insetY },
|
||||
{ x: rect.x1 - insetX, y: rect.y1 - 1 },
|
||||
{ x: rect.x0, y: rect.y1 - insetY },
|
||||
];
|
||||
}
|
||||
const terrainType = params.get("patchTerrain") || initial.map.terrainTemplate?.terrainType || "auto";
|
||||
const candidatePlan = Array.from({ length: candidateLimit }, (_, index) => {
|
||||
const variant = (startVariant + index) >>> 0;
|
||||
return { candidateId: `e2e:${variant}`, candidateOrdinal: index + 1, variant, seed: deriveSeed(world.seed, terrainType, variant) };
|
||||
});
|
||||
const patchWorker = new Worker(new URL("../src/mapPatchWorker.js", import.meta.url), { type: "module" });
|
||||
const patchStartedAt = performance.now();
|
||||
const patch = await waitForPatch(patchWorker, {
|
||||
id: 2,
|
||||
world,
|
||||
rect,
|
||||
options: {
|
||||
patchMode: params.get("mode") || "regeneration",
|
||||
terrainType,
|
||||
variant: startVariant,
|
||||
seed: candidatePlan[0].seed,
|
||||
maxQualityRetries: 0,
|
||||
qualityTerrainAttempts: 1,
|
||||
acceptBestAvailableQuality: false,
|
||||
includeSeamVisualization: false,
|
||||
},
|
||||
search: {
|
||||
searchId: "browser-e2e",
|
||||
operationId: "browser-e2e",
|
||||
committedRevision: 1,
|
||||
workerEpoch: 1,
|
||||
candidatePlan,
|
||||
totalCandidateCount: candidateLimit,
|
||||
},
|
||||
});
|
||||
const patchEndedAt = performance.now();
|
||||
const afterPatchHeap = heapSnapshot("after-patch");
|
||||
if (afterPatchHeap) heap.push(afterPatchHeap);
|
||||
if (patch.result?.ok !== true) {
|
||||
throw new Error(`No accepted preview was produced (${patch.result?.code || patch.result?.searchStatus || "unknown rejection"}).`);
|
||||
}
|
||||
const applyAck = await waitForApplyAck(patchWorker, patch);
|
||||
const afterApplyHeap = heapSnapshot("after-apply-ack");
|
||||
if (afterApplyHeap) heap.push(afterApplyHeap);
|
||||
patchWorker.terminate();
|
||||
const attempts = patch.result?.searchAttempts || [];
|
||||
const boundedEvents = patch.progress.filter((entry) => entry.boundedWork === true);
|
||||
const invalidBoundedEvents = boundedEvents.filter((entry) => !Number.isFinite(entry.completed)
|
||||
|| !Number.isFinite(entry.total) || entry.completed < 0 || entry.total < 0 || entry.completed > entry.total);
|
||||
const assertions = {
|
||||
workerTransportSucceeded: patch.ok === true,
|
||||
candidateAuditPresent: attempts.length > 0,
|
||||
previewPublished: patch.result?.ok === true && patch.result?.searchStatus === "succeeded",
|
||||
boundedAttempts: attempts.length <= candidateLimit,
|
||||
fullPipelineTimingsPresent: attempts.every((attempt) => (attempt.patchTimings || []).some((entry) => entry.key === "candidate" || entry.key === "tiled-total" || entry.key === "tiled-regeneration-total")),
|
||||
noBestAvailableAcceptance: attempts.every((attempt) => attempt.candidateQuality?.acceptedAsBestAvailable !== true),
|
||||
boundedProgressValid: boundedEvents.length > 0 && invalidBoundedEvents.length === 0,
|
||||
applyAckHashMatches: applyAck.mirrorHash === patch.result?.acceptedWorldHash,
|
||||
patchBudgetMet: patch.wallMs < patchBudgetMs,
|
||||
};
|
||||
const report = {
|
||||
status: Object.values(assertions).every(Boolean) ? "pass" : "fail",
|
||||
environment: {
|
||||
userAgent: navigator.userAgent,
|
||||
hardwareConcurrency: navigator.hardwareConcurrency || null,
|
||||
deviceMemoryGiB: navigator.deviceMemory || null,
|
||||
crossOriginIsolated,
|
||||
},
|
||||
workload: {
|
||||
seed, startVariant, candidateLimit, selection: rect, selectionWidth, selectionHeight,
|
||||
selectionShape: rect.kind || "rect", terrainType, patchMode: params.get("mode") || "regeneration",
|
||||
plannedTileUpperBound: Math.ceil(selectionWidth / Math.floor(258 / 1.72)) * Math.ceil(selectionHeight / Math.floor(183 / 1.72)),
|
||||
},
|
||||
timing: {
|
||||
initialWallMs: initialEndedAt - initialStartedAt,
|
||||
patchWallMs: patch.wallMs,
|
||||
patchBudgetMs,
|
||||
initialMaxProgressGapMs: maxProgressGap(initial.progress, initialStartedAt, initialEndedAt),
|
||||
patchMaxProgressGapMs: maxProgressGap(patch.progress, patchStartedAt, patchEndedAt),
|
||||
},
|
||||
memory: heap.length ? {
|
||||
snapshots: heap,
|
||||
peakUsedJSHeapSize: Math.max(...heap.map((entry) => entry.usedJSHeapSize)),
|
||||
} : null,
|
||||
assertions,
|
||||
result: {
|
||||
ok: patch.result?.ok === true,
|
||||
code: patch.result?.code || null,
|
||||
searchStatus: patch.result?.searchStatus || null,
|
||||
actualVariant: patch.result?.actualVariant ?? null,
|
||||
nextVariant: patch.result?.nextVariant ?? null,
|
||||
attempts,
|
||||
acceptedWorldHash: patch.result?.acceptedWorldHash || null,
|
||||
applyAck,
|
||||
},
|
||||
};
|
||||
document.documentElement.dataset.status = report.status;
|
||||
resultEl.textContent = JSON.stringify(report, null, 2);
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
document.documentElement.dataset.status = "fail";
|
||||
resultEl.textContent = JSON.stringify({ status: "fail", infrastructureError: error?.message || String(error), stack: error?.stack || null }, null, 2);
|
||||
}
|
||||
|
|
@ -2,50 +2,30 @@ import { Worker } from "node:worker_threads";
|
|||
import { performance } from "node:perf_hooks";
|
||||
import { generateMap } from "../src/mapPipeline.js";
|
||||
import { createWorldMap } from "../src/worldMap.js";
|
||||
import { buildMaximumProductionLasso, derivePatchSeed } from "./production-fixtures.mjs";
|
||||
|
||||
const budgetMs = Math.max(1, Number(process.env.PATCH_MAX_WORKER_BUDGET_MS) || 60_000);
|
||||
const timeoutMs = Math.max(90_000, budgetMs + 30_000);
|
||||
const worldSeed = Number(process.env.PATCH_TEST_WORLD_SEED ?? 114514) >>> 0;
|
||||
const candidateVariant = Number(process.env.PATCH_TEST_VARIANT ?? 0) >>> 0;
|
||||
|
||||
function deriveSeed(worldSeed, terrainType, variant) {
|
||||
let h = (worldSeed >>> 0) ^ 0x9e3779b9;
|
||||
h = Math.imul(h ^ (variant >>> 0), 668265263) >>> 0;
|
||||
for (const ch of String(terrainType || "auto")) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0;
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
const initialStartedAt = performance.now();
|
||||
const initial = generateMap(worldSeed);
|
||||
const initialGenerationMs = Math.round(performance.now() - initialStartedAt);
|
||||
const world = createWorldMap(initial);
|
||||
const width = 470;
|
||||
const height = 333;
|
||||
const x0 = Math.max(0, Math.min(world.width - width, world.originX + 238));
|
||||
const y0 = Math.max(0, Math.min(world.height - height, world.originY));
|
||||
const rect = { x0, y0, x1: x0 + width, y1: y0 + height, kind: "lasso" };
|
||||
const insetX = Math.max(4, Math.floor(width * 0.16));
|
||||
const insetY = Math.max(4, Math.floor(height * 0.16));
|
||||
rect.polygon = [
|
||||
{ x: rect.x0 + insetX, y: rect.y0 },
|
||||
{ x: rect.x1 - 1, y: rect.y0 + insetY },
|
||||
{ x: rect.x1 - insetX, y: rect.y1 - 1 },
|
||||
{ x: rect.x0, y: rect.y1 - insetY },
|
||||
];
|
||||
const rect = buildMaximumProductionLasso(world);
|
||||
|
||||
const terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
|
||||
const seed = deriveSeed(world.seed, terrainType, candidateVariant);
|
||||
const seed = derivePatchSeed(world.seed, terrainType, candidateVariant);
|
||||
const candidatePlan = [{ candidateId: `max-worker:${candidateVariant}`, candidateOrdinal: 1, variant: candidateVariant, seed }];
|
||||
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
|
||||
const startedAt = performance.now();
|
||||
let progressEvents = 0;
|
||||
let maxRssBytes = process.memoryUsage().rss;
|
||||
let maxHeapBytes = process.memoryUsage().heapUsed;
|
||||
let lastLabel = "";
|
||||
const memoryTimer = setInterval(() => {
|
||||
const memory = process.memoryUsage();
|
||||
maxRssBytes = Math.max(maxRssBytes, memory.rss);
|
||||
maxHeapBytes = Math.max(maxHeapBytes, memory.heapUsed);
|
||||
}, 100);
|
||||
|
||||
async function finish(exitCode, payload) {
|
||||
|
|
@ -68,7 +48,6 @@ async function finish(exitCode, payload) {
|
|||
budgetMs,
|
||||
withinBudget,
|
||||
maxRssBytes,
|
||||
maxHeapBytes,
|
||||
progressEvents,
|
||||
lastLabel,
|
||||
searchStatus: payload?.searchStatus || null,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
normalizePatchPrefectureCapitals,
|
||||
preparePatchTransactionFields,
|
||||
reconcileGeneratedHumanPointsWithFinalTerrain,
|
||||
regionalRecalculationProbability,
|
||||
restorePatchTransactionSnapshot,
|
||||
refreshPatchPrefectureMetadata,
|
||||
synchronizePatchAdministrativeMetadata,
|
||||
|
|
@ -37,6 +38,16 @@ function assert(condition, message) {
|
|||
console.log(`OK: ${message}`);
|
||||
}
|
||||
|
||||
const recalcAtSelection = regionalRecalculationProbability(0, 100);
|
||||
const recalcNear = regionalRecalculationProbability(20, 100);
|
||||
const recalcFar = regionalRecalculationProbability(70, 100);
|
||||
const recalcOutside = regionalRecalculationProbability(100, 100);
|
||||
assert(recalcAtSelection === 1
|
||||
&& recalcNear > recalcFar
|
||||
&& recalcFar > 0
|
||||
&& recalcOutside === 0,
|
||||
"regional recalculation probability rises monotonically toward the selected expansion area");
|
||||
|
||||
function testPointInPolygon(px, py, polygon) {
|
||||
let inside = false;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
|
|
@ -80,6 +91,66 @@ assert(search.result?.ok && search.result.actualVariant === 6, "content rejectio
|
|||
assert(calls.length === 2 && calls.every((call) => call.baseline === 1), "candidate worlds start from one immutable baseline");
|
||||
assert(progress.filter((event) => event.boundedWork).every((event) => event.completed <= event.total), "bounded progress remains inside its finite work total");
|
||||
|
||||
const bestOfThreeCalls = [];
|
||||
const bestOfThreeScores = new Map([[12, 0.31], [13, 0.88], [14, 0.57]]);
|
||||
const bestOfThree = runPatchCandidateSearch({
|
||||
id: 1001,
|
||||
world: base,
|
||||
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-best-of-three",
|
||||
selectBestCandidate: true,
|
||||
candidatePlan: [12, 13, 14].map((variant, index) => ({ candidateOrdinal: index + 1, candidateId: `best-${variant}`, variant, seed: 100 + variant })),
|
||||
},
|
||||
}, {
|
||||
cloneWorld: structuredClone,
|
||||
generateCandidate: (world, rect, options) => {
|
||||
bestOfThreeCalls.push(options.variant);
|
||||
world.fields.marker[0] = options.variant;
|
||||
return {
|
||||
ok: true,
|
||||
variant: options.variant,
|
||||
seed: options.seed,
|
||||
candidateQuality: { hardPass: false, score: bestOfThreeScores.get(options.variant) },
|
||||
seamDiagnostics: { hardPass: false, gateReasons: ["forced-diagnostic-only-seam"] },
|
||||
rects: { writeRect: rect },
|
||||
};
|
||||
},
|
||||
});
|
||||
assert(bestOfThree.result?.ok
|
||||
&& bestOfThree.result.bestOfCandidates === true
|
||||
&& bestOfThree.result.actualVariant === 13
|
||||
&& bestOfThree.result.candidateOrdinal === 2
|
||||
&& bestOfThreeCalls.join(",") === "12,13,14"
|
||||
&& bestOfThree.result.searchAttempts.map((attempt) => attempt.status).join(",") === "evaluated,success,evaluated",
|
||||
"best-of-three mode evaluates every complete candidate and publishes the highest quality even when all gates remain diagnostic failures");
|
||||
|
||||
const asyncBestOfThree = await runPatchCandidateSearchAsync({
|
||||
id: 1002,
|
||||
world: base,
|
||||
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-best-of-three-async",
|
||||
selectBestCandidate: true,
|
||||
candidatePlan: [21, 22, 23].map((variant, index) => ({ candidateOrdinal: index + 1, candidateId: `async-best-${variant}`, variant, seed: 200 + variant })),
|
||||
},
|
||||
}, {
|
||||
cloneWorld: structuredClone,
|
||||
generateCandidate: async (world, rect, options) => ({
|
||||
ok: true,
|
||||
variant: options.variant,
|
||||
seed: options.seed,
|
||||
candidateQuality: { hardPass: options.variant === 22, score: options.variant === 23 ? 0.93 : options.variant === 22 ? 0.71 : 0.42 },
|
||||
seamDiagnostics: { hardPass: options.variant === 22, gateReasons: options.variant === 22 ? [] : ["diagnostic-seam"] },
|
||||
rects: { writeRect: rect },
|
||||
}),
|
||||
});
|
||||
assert(asyncBestOfThree.result?.ok && asyncBestOfThree.result.actualVariant === 23
|
||||
&& asyncBestOfThree.result.searchAttempts.length === 3,
|
||||
"async best-of-three selection uses quality score as the primary ranking signal and evaluates all candidates");
|
||||
|
||||
const repeatedPipelineProgress = [];
|
||||
const repeatedPipelineSearch = runPatchCandidateSearch({
|
||||
id: 101,
|
||||
|
|
@ -383,6 +454,18 @@ assert(!ArrayBuffer.isView(transportDebugViewport.transportDebug.layers.expressw
|
|||
&& transportDebugViewport.transportDebug.layers.components.length === 1,
|
||||
"viewport drops unused transport potential rasters while preserving vector diagnostics");
|
||||
|
||||
const edgeViewport = getViewportMap({
|
||||
width: 3,
|
||||
height: 2,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
renderRevision: 1,
|
||||
fields: { sea: new Uint8Array([1, 2, 3, 4, 5, 6]) },
|
||||
sourceMap: {},
|
||||
}, { x: -1, y: -1 }, 4, 4);
|
||||
assert(Array.from(edgeViewport.sea).join(",") === "1,1,1,1,1,1,2,3,1,4,5,6,1,1,1,1",
|
||||
"viewport row copies preserve exact clipping and fallback cells at negative camera edges");
|
||||
|
||||
const arrayDeltaBase = {
|
||||
width: 1, height: 1,
|
||||
fields: { marker: new Uint8Array([1]) }, generatedMask: new Uint8Array(1),
|
||||
|
|
@ -540,6 +623,264 @@ assert(transactionalCalls.every(([sea, municipality, villages]) => sea === 0 &&
|
|||
&& JSON.stringify(transactionalSearchBase) === JSON.stringify(transactionOriginal),
|
||||
"rejected and accepted transactional candidates both start from and restore the same committed mirror");
|
||||
|
||||
const transactionalBestBase = structuredClone(transactionOriginal);
|
||||
const transactionalBestScores = new Map([[31, 0.44], [32, 0.91], [33, 0.68]]);
|
||||
const transactionalBestSearch = runPatchCandidateSearch({
|
||||
id: 303,
|
||||
world: transactionalBestBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
selectBestCandidate: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [31, 32, 33].map((variant, index) => ({ candidateOrdinal: index + 1, candidateId: `transactional-best-${variant}`, variant, seed: 300 + variant })),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
generateCandidate: (world, rect, options) => {
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.sourceMap.villages.push({ x: options.variant, y: 2 });
|
||||
world.patchGenerationSerial++;
|
||||
return {
|
||||
ok: true,
|
||||
rects: { writeRect: rect },
|
||||
candidateQuality: { hardPass: false, score: transactionalBestScores.get(options.variant) },
|
||||
seamDiagnostics: { hardPass: false, gateReasons: ["diagnostic-only"] },
|
||||
};
|
||||
},
|
||||
});
|
||||
const transactionalBestApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), transactionalBestSearch.transactionDelta);
|
||||
assert(transactionalBestSearch.result?.actualVariant === 32
|
||||
&& transactionalBestSearch.result?.candidateOrdinal === 2
|
||||
&& JSON.stringify(transactionalBestBase) === JSON.stringify(transactionOriginal)
|
||||
&& transactionalBestApplied.fields.sea[19] === 32
|
||||
&& transactionalBestApplied.fields.municipalityId[10] === 32
|
||||
&& transactionalBestApplied.sourceMap.villages.at(-1)?.x === 32,
|
||||
"transactional best-of-three keeps only the highest-quality candidate delta and restores the committed mirror after every evaluation");
|
||||
|
||||
const draftSelectionBase = structuredClone(transactionOriginal);
|
||||
const draftSelectionScores = new Map([[41, 0.41], [42, 0.94], [43, 0.63]]);
|
||||
const draftSelectionDraftCalls = [];
|
||||
const draftSelectionFullCalls = [];
|
||||
let draftSelectionDeltaBuilds = 0;
|
||||
let draftSelectionHashBuilds = 0;
|
||||
const draftSelectionProgress = [];
|
||||
const draftSelectionSearch = await runPatchCandidateSearchAsync({
|
||||
id: 304,
|
||||
world: draftSelectionBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-draft-ranked-production",
|
||||
selectBestCandidate: true,
|
||||
draftSelection: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [41, 42, 43].map((variant, index) => ({
|
||||
candidateOrdinal: index + 1, candidateId: `draft-ranked-${variant}`, variant, seed: 400 + variant,
|
||||
})),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
onProgress: (message) => draftSelectionProgress.push(message.progress),
|
||||
prepareOperationContext: () => ({ ok: true, signature: "unit-shared-context" }),
|
||||
evaluateDraftCandidate: async (world, rect, options) => {
|
||||
draftSelectionDraftCalls.push(options.variant);
|
||||
return {
|
||||
ok: true,
|
||||
score: draftSelectionScores.get(options.variant),
|
||||
qualityUpperBound: draftSelectionScores.get(options.variant),
|
||||
candidateQuality: { score: draftSelectionScores.get(options.variant), qualityUpperBound: draftSelectionScores.get(options.variant), terrain: { score: 0.8 }, human: { score: 0.7 } },
|
||||
reusableForFull: true,
|
||||
precomputedDraft: { terrain: { unitTerrainVariant: options.variant }, baseSeed: (400 + options.variant) >>> 0, effectiveSeed: (400 + options.variant) >>> 0, generationContext: { variant: options.variant }, generationTimings: [{ key: "terrain", ms: 1 }] },
|
||||
};
|
||||
},
|
||||
generateCandidate: async (world, rect, options) => {
|
||||
draftSelectionFullCalls.push({ variant: options.variant, reusedFullDraft: options._precomputedDraftCandidate != null, reusedTerrain: options._precomputedTerrainDraftCandidate?.generationContext?.variant ?? null });
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.sourceMap.villages.push({ x: options.variant, y: 3 });
|
||||
world.patchGenerationSerial++;
|
||||
return {
|
||||
ok: true,
|
||||
rects: { writeRect: rect },
|
||||
candidateQuality: { hardPass: true, score: 0.9 },
|
||||
seamDiagnostics: { hardPass: true },
|
||||
};
|
||||
},
|
||||
buildCommittedDeltaFromTransaction: (transaction, world) => {
|
||||
draftSelectionDeltaBuilds++;
|
||||
return buildCommittedMirrorDeltaFromTransaction(transaction, world);
|
||||
},
|
||||
hashCommittedWorld: (world) => {
|
||||
draftSelectionHashBuilds++;
|
||||
return hashCommittedWorld(world);
|
||||
},
|
||||
});
|
||||
const draftSelectionApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), draftSelectionSearch.transactionDelta);
|
||||
assert(draftSelectionSearch.result?.ok
|
||||
&& draftSelectionSearch.result.actualVariant === 42
|
||||
&& draftSelectionSearch.result.draftSelection?.policy === "admissible-terrain-scout-branch-and-bound-two-lane-v2"
|
||||
&& draftSelectionSearch.result.draftSelection?.fullCandidateCount === 1
|
||||
&& draftSelectionSearch.result.draftSelection?.reusedWinningDraft === false
|
||||
&& draftSelectionSearch.result.draftSelection?.reusedTerrainDraft === true
|
||||
&& draftSelectionSearch.result.draftSelection?.fullProductionFromTerrainOnly === true
|
||||
&& draftSelectionDraftCalls.join(",") === "41,42,43"
|
||||
&& draftSelectionFullCalls.length === 1
|
||||
&& draftSelectionFullCalls[0].variant === 42
|
||||
&& draftSelectionFullCalls[0].reusedFullDraft === false
|
||||
&& draftSelectionFullCalls[0].reusedTerrain === 42,
|
||||
"terrain-scout production evaluates three cheap terrain candidates, fully finalizes only the viable winner, and never reuses simplified human/transport draft stages");
|
||||
assert(draftSelectionDeltaBuilds === 1
|
||||
&& draftSelectionHashBuilds === 1
|
||||
&& draftSelectionApplied.fields.sea[19] === 42
|
||||
&& draftSelectionApplied.fields.municipalityId[10] === 42
|
||||
&& JSON.stringify(draftSelectionBase) === JSON.stringify(transactionOriginal),
|
||||
"draft-ranked production builds the committed delta and whole-world hash exactly once for the accepted finalist and restores the committed mirror");
|
||||
assert(draftSelectionProgress.filter((event) => event.boundedWork).every((event) => event.completed <= event.total)
|
||||
&& draftSelectionProgress.filter((event) => event.workUnitId?.startsWith("finalist-") && event.key === "finalist-generation").every((event) => event.total === 1),
|
||||
"draft-ranked production keeps finalist progress bounded per full candidate without leaving an incomplete multi-finalist work unit");
|
||||
|
||||
const nearTieBase = structuredClone(transactionOriginal);
|
||||
const nearTieDraftScores = new Map([[51, 0.900], [52, 0.885], [53, 0.61]]);
|
||||
const nearTieFullScores = new Map([[51, 0.78], [52, 0.93], [53, 0.64]]);
|
||||
const nearTieFullCalls = [];
|
||||
let nearTieDeltaBuilds = 0;
|
||||
let nearTieHashBuilds = 0;
|
||||
const nearTieSearch = await runPatchCandidateSearchAsync({
|
||||
id: 305,
|
||||
world: nearTieBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-draft-near-tie",
|
||||
selectBestCandidate: true,
|
||||
draftSelection: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [51, 52, 53].map((variant, index) => ({
|
||||
candidateOrdinal: index + 1, candidateId: `draft-near-${variant}`, variant, seed: 500 + variant,
|
||||
})),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
prepareOperationContext: () => ({ ok: true, signature: "unit-near-tie-context" }),
|
||||
evaluateDraftCandidate: async (world, rect, options) => {
|
||||
const upper = new Map([[51, 0.95], [52, 0.96], [53, 0.70]]).get(options.variant);
|
||||
return {
|
||||
ok: true,
|
||||
score: nearTieDraftScores.get(options.variant),
|
||||
qualityUpperBound: upper,
|
||||
candidateQuality: { score: nearTieDraftScores.get(options.variant), qualityUpperBound: upper, hardPass: true, terrain: { score: 0.8 }, human: { score: 0.7 } },
|
||||
reusableForFull: true,
|
||||
precomputedDraft: { terrain: { unitTerrainVariant: options.variant }, baseSeed: (500 + options.variant) >>> 0, effectiveSeed: (500 + options.variant) >>> 0, generationContext: { variant: options.variant }, generationTimings: [{ key: "terrain", ms: 1 }] },
|
||||
};
|
||||
},
|
||||
generateCandidate: async (world, rect, options) => {
|
||||
nearTieFullCalls.push({ variant: options.variant, reusedFullDraft: options._precomputedDraftCandidate != null, reusedTerrain: options._precomputedTerrainDraftCandidate?.generationContext?.variant ?? null });
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.sourceMap.villages.push({ x: options.variant, y: 7 });
|
||||
world.patchGenerationSerial++;
|
||||
return {
|
||||
ok: true,
|
||||
rects: { writeRect: rect },
|
||||
candidateQuality: { hardPass: true, score: nearTieFullScores.get(options.variant) },
|
||||
seamDiagnostics: { hardPass: true, gateReasons: [] },
|
||||
};
|
||||
},
|
||||
buildCommittedDeltaFromTransaction: (transaction, world) => {
|
||||
nearTieDeltaBuilds++;
|
||||
return buildCommittedMirrorDeltaFromTransaction(transaction, world);
|
||||
},
|
||||
hashCommittedWorld: (world) => {
|
||||
nearTieHashBuilds++;
|
||||
return hashCommittedWorld(world);
|
||||
},
|
||||
});
|
||||
const nearTieApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), nearTieSearch.transactionDelta);
|
||||
assert(nearTieSearch.result?.ok
|
||||
&& nearTieSearch.result.actualVariant === 52
|
||||
&& nearTieSearch.result.draftSelection?.policy === "admissible-terrain-scout-branch-and-bound-two-lane-v2"
|
||||
&& nearTieSearch.result.draftSelection?.selectionReason === "admissible-bound-winner-live"
|
||||
&& nearTieSearch.result.draftSelection?.fullCandidateCount === 2
|
||||
&& nearTieSearch.result.draftSelection?.fullGenerationPassCount === 2
|
||||
&& nearTieSearch.result.draftSelection?.branchBoundPrunedCandidateOrdinals.includes(3)
|
||||
&& nearTieFullCalls.map((row) => row.variant).join(",") === "51,52",
|
||||
"admissible Branch-and-Bound fully evaluates only candidates that can still beat the current best and removes the fixed near-tie heuristic");
|
||||
assert(nearTieDeltaBuilds === 1
|
||||
&& nearTieHashBuilds === 1
|
||||
&& nearTieApplied.fields.sea[19] === 52
|
||||
&& nearTieApplied.fields.municipalityId[10] === 52
|
||||
&& JSON.stringify(nearTieBase) === JSON.stringify(transactionOriginal),
|
||||
"near-tie comparison still constructs delta/hash only once for the final selected candidate and restores the committed mirror");
|
||||
|
||||
const draftFallbackBase = structuredClone(transactionOriginal);
|
||||
const draftFallbackCalls = [];
|
||||
let draftFallbackDeltaBuilds = 0;
|
||||
let draftFallbackHashBuilds = 0;
|
||||
const draftFallbackSearch = await runPatchCandidateSearchAsync({
|
||||
id: 306,
|
||||
world: draftFallbackBase,
|
||||
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
|
||||
options: { acceptBestAvailableQuality: true },
|
||||
search: {
|
||||
searchId: "unit-draft-content-fallback",
|
||||
selectBestCandidate: true,
|
||||
draftSelection: true,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan: [71, 72, 73].map((variant, index) => ({
|
||||
candidateOrdinal: index + 1, candidateId: `draft-fallback-${variant}`, variant, seed: 700 + variant,
|
||||
})),
|
||||
},
|
||||
}, {
|
||||
transactional: true,
|
||||
prepareOperationContext: () => ({ ok: true, signature: "unit-fallback-context" }),
|
||||
evaluateDraftCandidate: async (world, rect, options) => ({
|
||||
ok: true,
|
||||
score: options.variant === 71 ? 0.91 : options.variant === 72 ? 0.72 : 0.51,
|
||||
qualityUpperBound: options.variant === 71 ? 0.96 : options.variant === 72 ? 0.90 : 0.60,
|
||||
candidateQuality: { score: options.variant === 71 ? 0.91 : options.variant === 72 ? 0.72 : 0.51, qualityUpperBound: options.variant === 71 ? 0.96 : options.variant === 72 ? 0.90 : 0.60, hardPass: true },
|
||||
reusableForFull: true,
|
||||
precomputedDraft: { terrain: { unitTerrainVariant: options.variant }, baseSeed: (700 + options.variant) >>> 0, effectiveSeed: (700 + options.variant) >>> 0, generationContext: { variant: options.variant }, generationTimings: [{ key: "terrain", ms: 1 }] },
|
||||
}),
|
||||
generateCandidate: async (world, rect, options) => {
|
||||
draftFallbackCalls.push({ variant: options.variant, reusedFullDraft: options._precomputedDraftCandidate != null, reusedTerrain: options._precomputedTerrainDraftCandidate?.generationContext?.variant ?? null });
|
||||
preparePatchTransactionFields(options._externalTransactionSnapshot, world, rect);
|
||||
if (options.variant === 71) return { ok: false, code: "patch-quality-gate-failed", reason: "forced full-stage rejection" };
|
||||
world.fields.sea[19] = options.variant;
|
||||
world.fields.municipalityId[10] = options.variant;
|
||||
world.patchGenerationSerial++;
|
||||
return { ok: true, rects: { writeRect: rect }, candidateQuality: { hardPass: true, score: 0.82 }, seamDiagnostics: { hardPass: true } };
|
||||
},
|
||||
buildCommittedDeltaFromTransaction: (transaction, world) => {
|
||||
draftFallbackDeltaBuilds++;
|
||||
return buildCommittedMirrorDeltaFromTransaction(transaction, world);
|
||||
},
|
||||
hashCommittedWorld: (world) => {
|
||||
draftFallbackHashBuilds++;
|
||||
return hashCommittedWorld(world);
|
||||
},
|
||||
});
|
||||
const draftFallbackApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), draftFallbackSearch.transactionDelta);
|
||||
assert(draftFallbackSearch.result?.ok
|
||||
&& draftFallbackSearch.result.actualVariant === 72
|
||||
&& draftFallbackSearch.result.draftSelection?.selectionReason?.startsWith("admissible-bound")
|
||||
&& draftFallbackSearch.result.draftSelection?.fullCandidateCount === 2
|
||||
&& draftFallbackCalls.map((row) => row.variant).join(",") === "71,72"
|
||||
&& draftFallbackCalls[0].reusedFullDraft === false
|
||||
&& draftFallbackCalls[0].reusedTerrain === 71
|
||||
&& draftFallbackCalls[1].reusedFullDraft === false
|
||||
&& draftFallbackCalls[1].reusedTerrain === null,
|
||||
"clear terrain-scout winner reuses only exact terrain on the fast path and regenerates a discarded runner-up after content rejection");
|
||||
assert(draftFallbackDeltaBuilds === 1
|
||||
&& draftFallbackHashBuilds === 1
|
||||
&& draftFallbackApplied.fields.sea[19] === 72
|
||||
&& JSON.stringify(draftFallbackBase) === JSON.stringify(transactionOriginal),
|
||||
"content fallback preserves the one-delta/one-hash publication invariant");
|
||||
|
||||
const municipalityWorld = {
|
||||
width: 8, height: 6,
|
||||
fields: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
|
@ -232,7 +233,19 @@ class CdpBrowser {
|
|||
}
|
||||
}
|
||||
|
||||
export async function launchChromiumCdp({ executablePath = process.env.CHROMIUM_PATH || "/usr/bin/chromium" } = {}) {
|
||||
function defaultChromiumExecutable() {
|
||||
const candidates = process.platform === "win32"
|
||||
? [
|
||||
process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, "Programs", "Microsoft Edge", "Application", "msedge.exe"),
|
||||
process.env["PROGRAMFILES(X86)"] && join(process.env["PROGRAMFILES(X86)"], "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
process.env.PROGRAMFILES && join(process.env.PROGRAMFILES, "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
process.env.PROGRAMFILES && join(process.env.PROGRAMFILES, "Google", "Chrome", "Application", "chrome.exe"),
|
||||
]
|
||||
: ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome"];
|
||||
return candidates.filter(Boolean).find((candidate) => existsSync(candidate)) || candidates.filter(Boolean)[0];
|
||||
}
|
||||
|
||||
export async function launchChromiumCdp({ executablePath = process.env.CHROMIUM_PATH || defaultChromiumExecutable() } = {}) {
|
||||
const userDataDir = await mkdtemp(join(tmpdir(), "jmg-chromium-"));
|
||||
const child = spawn(executablePath, [
|
||||
"--headless=new",
|
||||
|
|
|
|||
36
tests/debug.log
Normal file
36
tests/debug.log
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
[0811/195718.540:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/195719.021:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/195751.212:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/195858.053:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200025.218:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200112.414:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200159.694:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200206.915:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200237.062:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200303.325:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200325.372:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200341.314:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200355.039:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200414.422:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/200430.422:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/201027.558:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/201052.753:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/203141.432:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204206.661:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204207.227:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204407.281:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204529.582:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204721.230:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204808.726:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204903.607:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204911.221:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/204942.084:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205011.188:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205041.144:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205101.640:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205120.979:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205145.386:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205255.007:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205255.511:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/205745.891:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
[0811/210101.304:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: アクセスが拒否されました。 (0x5)
|
||||
15
tests/helpers-generation-worker-node-wrapper.mjs
Normal file
15
tests/helpers-generation-worker-node-wrapper.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { parentPort } from 'node:worker_threads';
|
||||
|
||||
if (!parentPort) throw new Error('generation worker node wrapper requires parentPort');
|
||||
const pending = [];
|
||||
globalThis.self = {
|
||||
postMessage(data, transfer = []) {
|
||||
parentPort.postMessage(data, transfer);
|
||||
},
|
||||
};
|
||||
parentPort.on('message', (data) => {
|
||||
if (typeof globalThis.self.onmessage === 'function') globalThis.self.onmessage({ data });
|
||||
else pending.push(data);
|
||||
});
|
||||
await import('../src/generationWorker.js');
|
||||
while (pending.length) globalThis.self.onmessage({ data: pending.shift() });
|
||||
|
|
@ -2,36 +2,18 @@ import { Worker } from "node:worker_threads";
|
|||
import { performance } from "node:perf_hooks";
|
||||
import { generateMap } from "../src/mapPipeline.js";
|
||||
import { createWorldMap } from "../src/worldMap.js";
|
||||
import { buildMaximumProductionLasso, derivePatchSeed } from "./production-fixtures.mjs";
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
console.log(`OK: ${message}`);
|
||||
}
|
||||
|
||||
function deriveSeed(worldSeed, terrainType, variant) {
|
||||
let hash = (worldSeed >>> 0) ^ 0x9e3779b9;
|
||||
hash = Math.imul(hash ^ (variant >>> 0), 668265263) >>> 0;
|
||||
for (const char of String(terrainType || "auto")) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619) >>> 0;
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
const initial = generateMap(114514);
|
||||
const world = createWorldMap(initial);
|
||||
const width = 470;
|
||||
const height = 333;
|
||||
const x0 = Math.max(0, Math.min(world.width - width, world.originX + 238));
|
||||
const y0 = Math.max(0, Math.min(world.height - height, world.originY));
|
||||
const rect = { x0, y0, x1: x0 + width, y1: y0 + height, kind: "lasso" };
|
||||
const insetX = Math.max(4, Math.floor(width * 0.16));
|
||||
const insetY = Math.max(4, Math.floor(height * 0.16));
|
||||
rect.polygon = [
|
||||
{ x: rect.x0 + insetX, y: rect.y0 },
|
||||
{ x: rect.x1 - 1, y: rect.y0 + insetY },
|
||||
{ x: rect.x1 - insetX, y: rect.y1 - 1 },
|
||||
{ x: rect.x0, y: rect.y1 - insetY },
|
||||
];
|
||||
const rect = buildMaximumProductionLasso(world);
|
||||
const terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
|
||||
const seed = deriveSeed(world.seed, terrainType, 0);
|
||||
const seed = derivePatchSeed(world.seed, terrainType, 0);
|
||||
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
|
||||
let terminalMessage = null;
|
||||
let cancelStartedAt = 0;
|
||||
|
|
|
|||
|
|
@ -130,7 +130,12 @@ async function main() {
|
|||
x1: world.originX + 132,
|
||||
y1: world.originY + 118,
|
||||
};
|
||||
const candidate = { candidateId: "node-sync:1", candidateOrdinal: 1, variant: 1, seed: 0x51a7c3d3 };
|
||||
const candidates = [1, 2, 3].map((variant, index) => ({
|
||||
candidateId: `node-sync:${variant}`,
|
||||
candidateOrdinal: index + 1,
|
||||
variant,
|
||||
seed: (0x51a7c3d3 + Math.imul(index, 0x9e3779b9)) >>> 0,
|
||||
}));
|
||||
const resultPromise = waitFor(worker, (message) => message?.id === 92 && message?.type !== "progress", 180_000);
|
||||
worker.postMessage({
|
||||
id: 92,
|
||||
|
|
@ -139,11 +144,11 @@ async function main() {
|
|||
options: {
|
||||
patchMode: "regeneration",
|
||||
terrainType: "auto",
|
||||
variant: candidate.variant,
|
||||
seed: candidate.seed,
|
||||
variant: candidates[0].variant,
|
||||
seed: candidates[0].seed,
|
||||
maxQualityRetries: 0,
|
||||
qualityTerrainAttempts: 1,
|
||||
acceptBestAvailableQuality: false,
|
||||
acceptBestAvailableQuality: true,
|
||||
includeSeamVisualization: false,
|
||||
},
|
||||
search: {
|
||||
|
|
@ -152,14 +157,29 @@ async function main() {
|
|||
committedRevision: 1,
|
||||
workerEpoch: 1,
|
||||
executionAttempt: 1,
|
||||
totalCandidateCount: 1,
|
||||
totalCandidateCount: candidates.length,
|
||||
reuseCommittedMirror: true,
|
||||
candidatePlan: [candidate],
|
||||
resolvedPatchMode: "regeneration",
|
||||
selectBestCandidate: true,
|
||||
draftSelection: true,
|
||||
candidatePlan: candidates,
|
||||
},
|
||||
});
|
||||
const result = await resultPromise;
|
||||
assert(result.ok === true && result.result?.ok === true, "a cold-synchronized mirror can run a complete production patch without retransmitting world");
|
||||
assert(result.result?.acceptedWorldHash && result.result?.applyToken, "cold-synchronized candidate returns transactional hash and Apply token");
|
||||
assert(result.ok === true && result.result?.ok === true, "a cold-synchronized mirror can run draft-ranked production without retransmitting world");
|
||||
const draftDebug = result.result?.draftSelection;
|
||||
const exactBoundBehavior = draftDebug?.fullCandidateCount < 3
|
||||
? draftDebug?.branchBoundPrunedCount > 0
|
||||
: draftDebug?.fullCandidateCount === 3 && draftDebug?.branchBoundPrunedCount === 0 && draftDebug?.fullComparedCandidateOrdinals?.length === 3;
|
||||
assert(draftDebug?.enabled === true
|
||||
&& draftDebug?.draftCount === 3
|
||||
&& draftDebug?.fullCandidateCount >= 1
|
||||
&& draftDebug?.fullCandidateCount <= 3
|
||||
&& draftDebug?.reusedWinningDraft === false
|
||||
&& draftDebug?.fullProductionFromTerrainOnly === true
|
||||
&& exactBoundBehavior,
|
||||
"real Worker production ranks three terrain scouts, prunes only mathematically dominated candidates, and fully compares all candidates when admissible bounds overlap");
|
||||
assert(result.result?.acceptedWorldHash && result.result?.applyToken, "draft-ranked finalist returns transactional hash and Apply token");
|
||||
|
||||
const ackId = "node-sync-apply";
|
||||
const applyPromise = waitFor(worker, (message) => message?.type === "patch-apply-ack-result" && message.ackId === ackId);
|
||||
|
|
|
|||
21
tests/production-fixtures.mjs
Normal file
21
tests/production-fixtures.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export function derivePatchSeed(worldSeed, terrainType, variant) {
|
||||
let hash = (worldSeed >>> 0) ^ 0x9e3779b9;
|
||||
hash = Math.imul(hash ^ (variant >>> 0), 668265263) >>> 0;
|
||||
for (const char of String(terrainType || "auto")) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619) >>> 0;
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
export function buildMaximumProductionLasso(world, width = 470, height = 333) {
|
||||
const x0 = Math.max(0, Math.min(world.width - width, world.originX + 238));
|
||||
const y0 = Math.max(0, Math.min(world.height - height, world.originY));
|
||||
const rect = { x0, y0, x1: x0 + width, y1: y0 + height, kind: "lasso" };
|
||||
const insetX = Math.max(4, Math.floor(width * 0.16));
|
||||
const insetY = Math.max(4, Math.floor(height * 0.16));
|
||||
rect.polygon = [
|
||||
{ x: rect.x0 + insetX, y: rect.y0 },
|
||||
{ x: rect.x1 - 1, y: rect.y0 + insetY },
|
||||
{ x: rect.x1 - insetX, y: rect.y1 - 1 },
|
||||
{ x: rect.x0, y: rect.y1 - insetY },
|
||||
];
|
||||
return rect;
|
||||
}
|
||||
87
tests/r10-exact-production-worker.mjs
Normal file
87
tests/r10-exact-production-worker.mjs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { Worker } from "node:worker_threads";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { generateMap } from "../src/mapPipeline.js";
|
||||
import { createWorldMap } from "../src/worldMap.js";
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
console.log(`OK: ${message}`);
|
||||
}
|
||||
|
||||
const initial = generateMap(114514);
|
||||
const baseline = createWorldMap(initial);
|
||||
const rect = { x0: 90, y0: 55, x1: 165, y1: 120 };
|
||||
const baseSeed = 88001;
|
||||
const candidatePlan = [0, 1, 2].map((variant, index) => ({
|
||||
candidateId: `r10-exact:${variant}`,
|
||||
candidateOrdinal: index + 1,
|
||||
variant,
|
||||
seed: (baseSeed + Math.imul(variant, 2654435761)) >>> 0,
|
||||
}));
|
||||
|
||||
async function runSearch(draftSelection) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
|
||||
const startedAt = performance.now();
|
||||
worker.on("message", async (message) => {
|
||||
if (message?.type === "progress") return;
|
||||
await worker.terminate();
|
||||
resolve({ message, elapsedMs: Math.round(performance.now() - startedAt) });
|
||||
});
|
||||
worker.on("error", reject);
|
||||
worker.postMessage({
|
||||
id: draftSelection ? 1001 : 1002,
|
||||
world: structuredClone(baseline),
|
||||
rect,
|
||||
options: {
|
||||
patchMode: "regeneration",
|
||||
terrainType: "auto",
|
||||
variant: 0,
|
||||
seed: baseSeed,
|
||||
maxQualityRetries: 0,
|
||||
qualityTerrainAttempts: 1,
|
||||
acceptBestAvailableQuality: false,
|
||||
includeSeamVisualization: false,
|
||||
},
|
||||
search: {
|
||||
searchId: draftSelection ? "r10-exact-fast" : "r10-exact-exhaustive",
|
||||
operationId: "r10-exact-production",
|
||||
committedRevision: 1,
|
||||
workerEpoch: 1,
|
||||
executionAttempt: 1,
|
||||
totalCandidateCount: candidatePlan.length,
|
||||
selectBestCandidate: true,
|
||||
draftSelection,
|
||||
resolvedPatchMode: "regeneration",
|
||||
candidatePlan,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const fast = await runSearch(true);
|
||||
const exhaustive = await runSearch(false);
|
||||
assert(fast.message?.result?.ok === true && exhaustive.message?.result?.ok === true,
|
||||
"both optimized and exhaustive searches produce publishable production candidates");
|
||||
assert(fast.message.result.actualVariant === exhaustive.message.result.actualVariant,
|
||||
"terrain-scout Branch-and-Bound selects the same variant as exhaustive full-production search");
|
||||
assert(Math.abs(Number(fast.message.result.selectionScore) - Number(exhaustive.message.result.selectionScore)) <= 1e-12,
|
||||
"optimized search returns the exact same final production quality score as exhaustive search");
|
||||
assert(fast.message.result.draftSelection?.reusedWinningDraft === false
|
||||
&& fast.message.result.draftSelection?.fullProductionFromTerrainOnly === true,
|
||||
"optimized search never publishes or reuses simplified human/transport draft output");
|
||||
assert(fast.message.result.draftSelection?.parallelDraftGeneration === true
|
||||
&& fast.message.result.draftSelection?.parallelDraftLaneCount === 2,
|
||||
"two resident terrain-scout lanes are active in the real Worker path");
|
||||
assert(fast.message.result.draftSelection?.fullCandidateCount <= candidatePlan.length
|
||||
&& Number.isFinite(fast.message.result.draftSelection?.branchBoundPrunedCount)
|
||||
&& fast.message.result.draftSelection?.ranking?.every((row) => Number.isFinite(row.qualityUpperBound)),
|
||||
"admissible bounds never evaluate more full candidates than exhaustive search and retain explicit upper-bound audit data");
|
||||
console.log(JSON.stringify({
|
||||
optimizedMs: fast.elapsedMs,
|
||||
exhaustiveMs: exhaustive.elapsedMs,
|
||||
optimizedFullCandidates: fast.message.result.draftSelection?.fullCandidateCount,
|
||||
prunedCandidates: fast.message.result.draftSelection?.branchBoundPrunedCount,
|
||||
variant: fast.message.result.actualVariant,
|
||||
score: fast.message.result.selectionScore,
|
||||
}, null, 2));
|
||||
102
tests/r11-selection-native-production.mjs
Normal file
102
tests/r11-selection-native-production.mjs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { generateMap } from "../src/mapPipeline.js";
|
||||
import { createWorldMap } from "../src/worldMap.js";
|
||||
import { buildMaximumProductionLasso, derivePatchSeed } from "./production-fixtures.mjs";
|
||||
|
||||
const worldSeed = 114514;
|
||||
const candidateVariant = 0;
|
||||
|
||||
const initial = generateMap(worldSeed);
|
||||
const world = createWorldMap(initial);
|
||||
const rect = buildMaximumProductionLasso(world);
|
||||
|
||||
const terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
|
||||
const seed = derivePatchSeed(world.seed, terrainType, candidateVariant);
|
||||
const candidatePlan = [{ candidateId: `r11-selection-native:${candidateVariant}`, candidateOrdinal: 1, variant: candidateVariant, seed }];
|
||||
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
|
||||
const timeoutMs = 90_000;
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`r11 selection-native worker timed out after ${timeoutMs} ms`)), timeoutMs);
|
||||
worker.on("message", (message) => {
|
||||
if (message.id !== 1 || message.type === "progress") return;
|
||||
clearTimeout(timer);
|
||||
if (!message.ok) reject(new Error(message.error || message.code || "r11 worker failed"));
|
||||
else resolve(message.result);
|
||||
});
|
||||
worker.on("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
worker.postMessage({
|
||||
id: 1,
|
||||
world,
|
||||
rect,
|
||||
options: {
|
||||
patchMode: "expansion",
|
||||
terrainType,
|
||||
variant: candidateVariant,
|
||||
seed,
|
||||
maxQualityRetries: 0,
|
||||
qualityTerrainAttempts: 1,
|
||||
acceptBestAvailableQuality: false,
|
||||
includeSeamVisualization: false,
|
||||
},
|
||||
search: {
|
||||
searchId: "r11-selection-native-production",
|
||||
operationId: "r11-selection-native-production",
|
||||
committedRevision: 1,
|
||||
workerEpoch: 1,
|
||||
executionAttempt: 1,
|
||||
totalCandidateCount: 1,
|
||||
candidatePlan,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(result?.ok, true, "candidate must complete successfully");
|
||||
assert.equal(result?.searchStatus, "succeeded", "candidate search must succeed");
|
||||
assert.equal(result?.selectionNativeProduction, true, "large Expansion must report selection-native production");
|
||||
assert.equal(result?.patchGenerationMode, "selection-native-chunked-production-v1", "large Expansion must expose r11 generation mode");
|
||||
assert.equal(result?.candidateQuality?.hardPass, true, "aggregate production quality gate must pass");
|
||||
assert.equal(result?.candidateQuality?.finalMerge?.hardPass, true, "final merged production quality must pass");
|
||||
assert.equal(result?.candidateQuality?.finalMerge?.transportHierarchyPass, true, "transport hierarchy Oracle must pass");
|
||||
assert.equal(result?.candidateQuality?.finalMerge?.transportTopology?.hardPass, true, "transport topology/service quality must pass");
|
||||
assert.equal(result?.candidateQuality?.finalMerge?.prefectureCoherence?.hardPass, true, "prefecture regional coherence quality must pass");
|
||||
assert.equal(result?.seamDiagnostics?.hardPass, true, "whole-selection seam audit must pass");
|
||||
|
||||
const nativeTransport = result?.seamDiagnostics?.aggregateTransportRepair?.selectionNativeTransport;
|
||||
assert.equal(nativeTransport?.policy, "whole-selection-post-admin-transport-v2-coherent-graph", "r11 whole-selection transport finalizer must be authoritative");
|
||||
assert.equal(nativeTransport?.chunkPostAdminTransportDeferred, true, "private chunks must defer duplicate post-admin transport finalization");
|
||||
assert.equal(nativeTransport?.fullResolutionRouting, true, "published transport must use full-resolution routing");
|
||||
assert.ok((nativeTransport?.roadGraphRepair?.roadGraphAfterComponents || 0) <= (nativeTransport?.roadGraphRepair?.roadGraphBeforeComponents || 0), "whole-selection road graph repair must not increase fragmentation");
|
||||
assert.ok((nativeTransport?.railGraphRepair?.railGraphAfterComponents || 0) <= (nativeTransport?.railGraphRepair?.railGraphBeforeComponents || 0), "whole-selection rail graph repair must not increase fragmentation");
|
||||
for (const classDebug of Object.values(nativeTransport?.regionalTrunkTransport?.byClass || {})) {
|
||||
if (!classDebug?.demand) continue;
|
||||
assert.equal(classDebug.mandatoryServiceConnected, classDebug.mandatoryServiceNodes, "every mandatory major-city/capital trunk service must be connected");
|
||||
if (classDebug.classGraph?.roadGraphBeforeComponents != null) assert.ok(classDebug.classGraph.roadGraphAfterComponents <= classDebug.classGraph.roadGraphBeforeComponents, "road hierarchy graph must not become more fragmented");
|
||||
if (classDebug.classGraph?.railGraphBeforeComponents != null) assert.ok(classDebug.classGraph.railGraphAfterComponents <= classDebug.classGraph.railGraphBeforeComponents, "rail hierarchy graph must not become more fragmented");
|
||||
}
|
||||
|
||||
const classes = result?.candidateQuality?.finalMerge?.transportClasses || {};
|
||||
const requirements = result?.candidateQuality?.finalMerge?.transportRequirements || {};
|
||||
for (const key of ["national", "expressway", "railTrunk"]) {
|
||||
if (!requirements[key]?.demand) continue;
|
||||
assert.ok((classes[key]?.cells || 0) >= (requirements[key]?.minCells || 0), `${key} routed-coverage floor must be met`);
|
||||
assert.ok((classes[key]?.paths || 0) > 0, `${key} must still contain at least one production corridor when demanded`);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
patchGenerationMode: result.patchGenerationMode,
|
||||
selectionNativePolicy: result.selectionNativePolicy,
|
||||
finalScore: result.candidateQuality.finalMerge.score,
|
||||
transportClasses: result.candidateQuality.finalMerge.transportClasses,
|
||||
transportRequirements: result.candidateQuality.finalMerge.transportRequirements,
|
||||
selectionNativeTransport: nativeTransport,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await worker.terminate();
|
||||
}
|
||||
81
tests/r11.4-literal-initial-overscan-worker.mjs
Normal file
81
tests/r11.4-literal-initial-overscan-worker.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
function pathLength(paths) {
|
||||
return (paths || []).reduce((sum, path) => sum + Math.max(0, (path?.length || 0) - 1), 0);
|
||||
}
|
||||
function prefMunicipalityCounts(map) {
|
||||
const groups = new Map();
|
||||
for (let i = 0; i < map.adminId.length; i++) {
|
||||
if (map.sea[i]) continue;
|
||||
const a = Number(map.adminId[i]), p = Number(map.prefectureRegionId[i]);
|
||||
if (a < 0 || p < 0) continue;
|
||||
if (!groups.has(p)) groups.set(p, new Set());
|
||||
groups.get(p).add(a);
|
||||
}
|
||||
return [...groups.values()].map((s) => s.size);
|
||||
}
|
||||
function edgeTouches(paths, width, height) {
|
||||
let hits = 0;
|
||||
for (const path of paths || []) {
|
||||
if ((path || []).some(([x, y]) => x <= 0 || y <= 0 || x >= width - 1 || y >= height - 1)) hits++;
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
function runWorker(seed) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL('./helpers-generation-worker-node-wrapper.mjs', import.meta.url), { type: 'module' });
|
||||
const timer = setTimeout(() => { worker.terminate(); reject(new Error(`worker timeout for seed ${seed}`)); }, 90000);
|
||||
worker.on('error', (error) => { clearTimeout(timer); reject(error); });
|
||||
worker.on('message', (message) => {
|
||||
if (message?.type !== 'result' || message?.id !== seed) return;
|
||||
clearTimeout(timer);
|
||||
worker.terminate();
|
||||
if (!message.ok) reject(new Error(message.error || 'generation failed'));
|
||||
else resolve(message.map);
|
||||
});
|
||||
worker.postMessage({ id: seed, seed, options: { terrainType: 'auto' } });
|
||||
});
|
||||
}
|
||||
|
||||
for (const seed of [114514, 999]) {
|
||||
const map = await runWorker(seed);
|
||||
assert.equal(map.width, 258, `seed ${seed}: published width`);
|
||||
assert.equal(map.height, 183, `seed ${seed}: published height`);
|
||||
const over = map.initialGenerationOverscan;
|
||||
assert.equal(over?.version, 'literal-hidden-raster-center-crop-v1', `seed ${seed}: literal overscan marker`);
|
||||
assert(over.fullWidth > map.width && over.fullHeight > map.height, `seed ${seed}: production was generated on a larger raster`);
|
||||
assert(over.marginX >= 40 && over.marginY >= 40, `seed ${seed}: meaningful hidden halo exists`);
|
||||
const visibleSize = map.width * map.height;
|
||||
const hiddenSize = over.fullWidth * over.fullHeight;
|
||||
for (const key of ['sea','elevation','slope','populationDensity','landuse','adminId','prefectureRegionId']) {
|
||||
assert.equal(map[key]?.length, visibleSize, `seed ${seed}: authoritative raster ${key} is center-cropped`);
|
||||
}
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
if (ArrayBuffer.isView(value)) assert.notEqual(value.length, hiddenSize, `seed ${seed}: hidden full raster ${key} is not leaked to published map`);
|
||||
}
|
||||
for (let id = 0; id < (map.adminCenters || []).length; id++) {
|
||||
const c = map.adminCenters[id];
|
||||
if (!c) continue;
|
||||
assert(c.x >= 0 && c.y >= 0 && c.x < map.width && c.y < map.height, `seed ${seed}: visible municipality seat is in crop`);
|
||||
assert.equal(map.adminId[Math.round(c.y) * map.width + Math.round(c.x)], id, `seed ${seed}: municipal seat belongs to municipality ${id}`);
|
||||
}
|
||||
const pops = (map.adminCenters || []).filter(Boolean).map((c) => Number(c.municipalityPopulation || 0));
|
||||
assert(pops.length >= 30, `seed ${seed}: sufficient municipalities survive central crop`);
|
||||
assert(Math.min(...pops) >= 1000, `seed ${seed}: municipal population floor`);
|
||||
assert(pops.filter((p) => p > 10000).length / pops.length <= 0.30, `seed ${seed}: >10k municipalities are minority`);
|
||||
const cityNames = (map.adminCenters || []).filter(Boolean).map((c) => String(c.name || ''));
|
||||
assert(cityNames.filter((n) => n.endsWith('市')).length / cityNames.length <= 0.35, `seed ${seed}: 市 does not dominate`);
|
||||
const prefCounts = prefMunicipalityCounts(map);
|
||||
assert(prefCounts.length >= 2 && Math.min(...prefCounts) >= 10, `seed ${seed}: every visible prefecture has substantial municipal subdivision`);
|
||||
const nationalLen = pathLength(map.nationalRoads);
|
||||
const railLen = pathLength([...(map.railways || []), ...(map.branchRailways || [])]);
|
||||
assert(nationalLen > 0 && railLen / nationalLen >= 0.70 && railLen / nationalLen <= 1.05, `seed ${seed}: published rail network is dense and close to national-road density`);
|
||||
const service = map.transportDebug?.postAdminTransportFinalization?.visibleCropMajorCityService;
|
||||
assert(service && service.checked >= 1, `seed ${seed}: published major-city service is audited`);
|
||||
assert.equal(service.missing.length, 0, `seed ${seed}: no interior visible major city lacks national+rail+expressway service`);
|
||||
const trunkBoundaryHits = edgeTouches([...(map.nationalRoads || []), ...(map.railways || []), ...(map.branchRailways || []), ...(map.expressways || [])], map.width, map.height);
|
||||
assert(trunkBoundaryHits >= 2, `seed ${seed}: real hidden-context trunk corridors cross the published crop boundary`);
|
||||
}
|
||||
|
||||
console.log('All r11.4 literal initial-overscan worker checks passed.');
|
||||
106
tests/r11.4-transport-demography-overscan.mjs
Normal file
106
tests/r11.4-transport-demography-overscan.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import fs from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { generateMap } from '../src/mapPipeline.js';
|
||||
|
||||
function prefectureMunicipalityCounts(map) {
|
||||
const groups = new Map();
|
||||
for (let i = 0; i < map.adminId.length; i++) {
|
||||
if (map.sea[i] || map.adminId[i] < 0 || map.prefectureRegionId[i] < 0) continue;
|
||||
let set = groups.get(map.prefectureRegionId[i]);
|
||||
if (!set) groups.set(map.prefectureRegionId[i], set = new Set());
|
||||
set.add(map.adminId[i]);
|
||||
}
|
||||
return [...groups.values()].map((set) => set.size);
|
||||
}
|
||||
function pathLength(paths) { return (paths || []).reduce((sum, path) => sum + (path?.length || 0), 0); }
|
||||
function maxSeaRun(path, sea, width) {
|
||||
let run = 0, best = 0;
|
||||
for (const [x, y] of path || []) {
|
||||
const isSea = x < 0 || y < 0 || x >= width || y >= sea.length / width || sea[y * width + x];
|
||||
if (isSea) { run++; best = Math.max(best, run); } else run = 0;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
function maxExtremeTurnRun(path) {
|
||||
let run = 0, best = 0;
|
||||
for (let k = 2; k < (path?.length || 0) - 2; k += 2) {
|
||||
const a = path[k - 2], b = path[k], c = path[k + 2];
|
||||
const ux = b[0] - a[0], uy = b[1] - a[1], vx = c[0] - b[0], vy = c[1] - b[1];
|
||||
const ud = Math.hypot(ux, uy), vd = Math.hypot(vx, vy);
|
||||
if (!ud || !vd) { run = 0; continue; }
|
||||
const dot = Math.max(-1, Math.min(1, (ux * vx + uy * vy) / (ud * vd)));
|
||||
const angle = Math.acos(dot) * 180 / Math.PI;
|
||||
if (angle >= 82) { run++; best = Math.max(best, run); } else run = 0;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function pathTangent(path, k) {
|
||||
const a = path[Math.max(0, k - 2)], b = path[Math.min(path.length - 1, k + 2)];
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1], d = Math.hypot(dx, dy) || 1;
|
||||
return [dx / d, dy / d];
|
||||
}
|
||||
function longestDistinctParallelRun(paths, radius) {
|
||||
let worst = 0;
|
||||
for (let a = 0; a < (paths?.length || 0); a++) for (let b = a + 1; b < paths.length; b++) {
|
||||
let run = 0;
|
||||
for (let k = 0; k < paths[a].length; k += 2) {
|
||||
const p = paths[a][k], t = pathTangent(paths[a], k);
|
||||
let parallel = false;
|
||||
for (let q = 0; q < paths[b].length; q += 2) {
|
||||
const z = paths[b][q], dx = p[0] - z[0], dy = p[1] - z[1], d2 = dx * dx + dy * dy;
|
||||
// Exact shared alignment is intentional multiplexing, not parallel duplication.
|
||||
if (d2 < 0.75 || d2 > radius * radius) continue;
|
||||
const u = pathTangent(paths[b], q);
|
||||
if (Math.abs(t[0] * u[0] + t[1] * u[1]) >= 0.90) { parallel = true; break; }
|
||||
}
|
||||
run = parallel ? run + 1 : 0;
|
||||
worst = Math.max(worst, run);
|
||||
}
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
|
||||
for (const seed of [114514, 999]) {
|
||||
const map = generateMap(seed, { onProgress() {}, initialGenerationOverscan: false });
|
||||
const populations = (map.adminCenters || []).map((p) => Number(p.municipalityPopulation || 0));
|
||||
assert(populations.length >= 30, `seed ${seed}: enough municipalities`);
|
||||
assert(Math.min(...populations) >= 1000, `seed ${seed}: municipality population floor is at least ~1000`);
|
||||
assert(populations.filter((p) => p > 10000).length / populations.length <= 0.30, `seed ${seed}: >10k municipalities are not dominant`);
|
||||
const names = (map.adminCenters || []).map((p) => String(p.name || ''));
|
||||
assert(names.filter((n) => n.endsWith('市')).length / names.length <= 0.35, `seed ${seed}: 市 does not dominate municipality types`);
|
||||
const prefCounts = prefectureMunicipalityCounts(map);
|
||||
assert(prefCounts.length >= 2 && Math.min(...prefCounts) >= 10, `seed ${seed}: no tiny five-municipality prefecture`);
|
||||
|
||||
const finalTransport = map.transportDebug?.postAdminTransportFinalization || {};
|
||||
assert.equal(finalTransport.postDedupeMajorCityService?.missing?.length || 0, 0, `seed ${seed}: all same-land major cities have national/rail/expressway service`);
|
||||
assert.equal(map.transportDebug?.layers?.syntheticUrbanStreetMeshDisabled, true, `seed ${seed}: synthetic urban grid is disabled`);
|
||||
assert.equal(map.transportDebug?.layers?.urbanStreetMeshes?.algorithm, 'existing-local-access', `seed ${seed}: urban local roads reuse the existing local-access algorithm`);
|
||||
|
||||
const nationalLen = pathLength(map.nationalRoads);
|
||||
const railLen = pathLength([...(map.railways || []), ...(map.branchRailways || [])]);
|
||||
assert(nationalLen > 0 && railLen / nationalLen >= 0.65 && railLen / nationalLen <= 1.02, `seed ${seed}: rail density is high but approximately below national-road density`);
|
||||
|
||||
for (const path of map.expressways || []) {
|
||||
assert(maxSeaRun(path, map.sea, map.width) <= 3, `seed ${seed}: expressway does not ignore a large strait`);
|
||||
assert(maxExtremeTurnRun(path) <= 1, `seed ${seed}: expressway avoids repeated extreme turns`);
|
||||
}
|
||||
const branch = finalTransport.finalExpresswayBranchSimplification;
|
||||
assert(branch && branch.branchNodesAfter <= branch.branchNodesBefore, `seed ${seed}: expressway branch simplifier never increases branching`);
|
||||
assert(finalTransport.postServiceNationalSharedAlignment && finalTransport.postServiceExpresswaySharedAlignment, `seed ${seed}: final same-direction corridor sharing pass is active`);
|
||||
assert(longestDistinctParallelRun(map.expressways || [], 4) <= 5, `seed ${seed}: expressways do not remain in long close parallel corridors`);
|
||||
assert(longestDistinctParallelRun(map.nationalRoads || [], 3) <= 5, `seed ${seed}: national roads do not remain in long close parallel corridors`);
|
||||
}
|
||||
|
||||
const featureSource = fs.readFileSync(new URL('../src/mapFeatures.js', import.meta.url), 'utf8');
|
||||
const rendererSource = fs.readFileSync(new URL('../src/renderer.js', import.meta.url), 'utf8');
|
||||
assert(/allowMillionPlusMap\s*=.*<\s*0\.50/.test(featureSource), 'million-plus city map-level probability gate is 50%');
|
||||
assert(rendererSource.includes('municipalFallbackLabels') && rendererSource.includes('!p.suppressMunicipalLabel') && rendererSource.includes('!p.seatOutsideVisibleCrop'), 'renderer suppresses municipal-seat labels whose true seat is outside the published crop instead of pinning them to a visible representative cell');
|
||||
|
||||
const generationWorkerSource = fs.readFileSync(new URL('../src/generationWorker.js', import.meta.url), 'utf8');
|
||||
const cropSource = fs.readFileSync(new URL('../src/initialGenerationCrop.js', import.meta.url), 'utf8');
|
||||
assert(generationWorkerSource.includes('__JAPAN_MAP_GENERATION_DIMENSIONS__') && generationWorkerSource.includes('cropInitialGenerationMap'), 'production initial generation uses a genuinely larger raster then crops the center');
|
||||
assert(cropSource.includes('literal-hidden-raster-center-crop-v1'), 'published initial map records literal hidden-raster overscan provenance');
|
||||
assert(cropSource.includes('suppressMunicipalLabel: true') && cropSource.includes('seatOutsideVisibleCrop: true'), 'crop metadata marks off-screen municipal seats as non-label anchors');
|
||||
|
||||
console.log('All r11.4 transport/demography/overscan regression checks passed.');
|
||||
150
tests/r11.5-visible-quality-finalizer.mjs
Normal file
150
tests/r11.5-visible-quality-finalizer.mjs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
function runWorker(seed) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL('./helpers-generation-worker-node-wrapper.mjs', import.meta.url), { type: 'module' });
|
||||
const timer = setTimeout(() => { worker.terminate(); reject(new Error(`worker timeout seed ${seed}`)); }, 120000);
|
||||
worker.on('error', (error) => { clearTimeout(timer); reject(error); });
|
||||
worker.on('message', async (message) => {
|
||||
if (message?.type !== 'result' || message.id !== seed) return;
|
||||
clearTimeout(timer);
|
||||
worker.removeAllListeners();
|
||||
await worker.terminate();
|
||||
if (!message.ok) reject(new Error(message.error || 'generation failed'));
|
||||
else resolve(message.map);
|
||||
});
|
||||
worker.postMessage({ id: seed, seed, options: { terrainType: 'auto' } });
|
||||
});
|
||||
}
|
||||
function pathLength(paths) {
|
||||
return (paths || []).reduce((sum, path) => {
|
||||
let length = 0;
|
||||
for (let i = 1; i < (path?.length || 0); i++) length += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]);
|
||||
return sum + length;
|
||||
}, 0);
|
||||
}
|
||||
function prefCounts(map) {
|
||||
const groups = new Map();
|
||||
for (let i = 0; i < map.adminId.length; i++) {
|
||||
if (map.sea[i]) continue;
|
||||
const a = Number(map.adminId[i]), p = Number(map.prefectureRegionId[i]);
|
||||
if (a < 0 || p < 0) continue;
|
||||
if (!groups.has(p)) groups.set(p, new Set());
|
||||
groups.get(p).add(a);
|
||||
}
|
||||
return [...groups.values()].map((s) => s.size);
|
||||
}
|
||||
function pathTangent(path, k) {
|
||||
const a = path[Math.max(0, k - 2)], b = path[Math.min(path.length - 1, k + 2)];
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1], d = Math.hypot(dx, dy) || 1;
|
||||
return [dx / d, dy / d];
|
||||
}
|
||||
function parallelRun(paths, radius) {
|
||||
let worst = 0;
|
||||
for (let a = 0; a < (paths?.length || 0); a++) for (let b = a + 1; b < paths.length; b++) {
|
||||
let run = 0;
|
||||
for (let k = 0; k < paths[a].length; k += 2) {
|
||||
const p = paths[a][k], t = pathTangent(paths[a], k); let parallel = false;
|
||||
for (let q = 0; q < paths[b].length; q += 2) {
|
||||
const z = paths[b][q], dx = p[0] - z[0], dy = p[1] - z[1], d2 = dx * dx + dy * dy;
|
||||
if (d2 < 0.75 || d2 > radius * radius) continue;
|
||||
const u = pathTangent(paths[b], q);
|
||||
if (Math.abs(t[0] * u[0] + t[1] * u[1]) >= 0.90) { parallel = true; break; }
|
||||
}
|
||||
run = parallel ? run + 1 : 0; worst = Math.max(worst, run);
|
||||
}
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
function extremeRun(path) {
|
||||
let run = 0, best = 0;
|
||||
for (let k = 2; k < (path?.length || 0) - 2; k += 2) {
|
||||
const a = path[k - 2], b = path[k], c = path[k + 2];
|
||||
const ux = b[0] - a[0], uy = b[1] - a[1], vx = c[0] - b[0], vy = c[1] - b[1];
|
||||
const ud = Math.hypot(ux, uy), vd = Math.hypot(vx, vy);
|
||||
if (!ud || !vd) { run = 0; continue; }
|
||||
const dot = Math.max(-1, Math.min(1, (ux * vx + uy * vy) / (ud * vd)));
|
||||
const angle = Math.acos(dot) * 180 / Math.PI;
|
||||
run = angle >= 82 ? run + 1 : 0; best = Math.max(best, run);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
function edgeHits(paths, width, height) {
|
||||
return (paths || []).filter((path) => (path || []).some(([x, y]) => x <= 0 || y <= 0 || x >= width - 1 || y >= height - 1)).length;
|
||||
}
|
||||
function pointPathDistance(point, path) {
|
||||
let best = Infinity;
|
||||
for (const tuple of path || []) best = Math.min(best, Math.hypot(tuple[0] - point.x, tuple[1] - point.y));
|
||||
return best;
|
||||
}
|
||||
function clearInteriorMinorOrphans(map) {
|
||||
const minor = map.minorRoads || [];
|
||||
const trunks = [...(map.nationalRoads || []), ...(map.externalRoads || []), ...(map.expressways || []), ...(map.railways || []), ...(map.branchRailways || [])];
|
||||
const civic = [...(map.modernCities || []), ...(map.markets || []), ...(map.villages || []), ...(map.ports || []), ...(map.adminCenters || []).filter(Boolean)];
|
||||
const connected = (tuple, self) => {
|
||||
const pt = { x: tuple[0], y: tuple[1] };
|
||||
if (trunks.some((path) => pointPathDistance(pt, path) <= 2.5)) return true;
|
||||
return minor.some((path, index) => index !== self && pointPathDistance(pt, path) <= 2.2);
|
||||
};
|
||||
const out = [];
|
||||
for (let index = 0; index < minor.length; index++) {
|
||||
const path = minor[index];
|
||||
if (!path?.length) continue;
|
||||
let length = 0;
|
||||
for (let k = 1; k < path.length; k++) length += Math.hypot(path[k][0] - path[k - 1][0], path[k][1] - path[k - 1][1]);
|
||||
if (length >= 24) continue;
|
||||
if (path.some(([x, y]) => x <= 0.5 || y <= 0.5 || x >= map.width - 1.5 || y >= map.height - 1.5)) continue;
|
||||
if (civic.some((point) => pointPathDistance(point, path) <= 2.6)) continue;
|
||||
const a = path[0], b = path[path.length - 1];
|
||||
if (!connected(a, index) && !connected(b, index)) out.push({ index, length });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
for (const seed of [1, 2, 5]) {
|
||||
console.log('r11.5 audit seed', seed);
|
||||
const map = await runWorker(seed);
|
||||
console.log('r11.5 generated seed', seed);
|
||||
const post = map.transportDebug?.postAdminTransportFinalization || {};
|
||||
const visible = post.visibleCropMajorCityService;
|
||||
const hidden = post.postDedupeMajorCityService;
|
||||
const visibleFinalizer = post.initialVisibleCropFinalizer;
|
||||
|
||||
assert.equal(map.initialGenerationOverscan?.version, 'literal-hidden-raster-center-crop-v1', `seed ${seed}: literal production overscan`);
|
||||
assert(visibleFinalizer?.productionOnly && visibleFinalizer?.simplifiedOutputForbidden, `seed ${seed}: visible finalizer uses full production output only`);
|
||||
assert(post.visibleCropOrphanMinorRoadCleanup?.enabled === true, `seed ${seed}: crop-induced minor-road orphan cleanup ran on published raster`);
|
||||
assert.equal(clearInteriorMinorOrphans(map).length, 0, `seed ${seed}: no short crop-created interior minor road is disconnected at both ends without serving a settlement`);
|
||||
assert.equal(visible?.missing?.length || 0, 0, `seed ${seed}: every publishable major city gets all required trunk modes`);
|
||||
assert.equal(visible?.edgeTruncated?.length || 0, 0, `seed ${seed}: crop-edge is no longer an excuse for missing major-city trunk service`);
|
||||
for (const ex of visible?.geographicExceptions || []) {
|
||||
assert(ex.visibleLandComponentArea < 96 && ex.national && ex.rail && !ex.expressway, `seed ${seed}: only tiny-islet motorway omission is allowed`);
|
||||
}
|
||||
assert.equal(hidden?.missing?.length || 0, 0, `seed ${seed}: hidden production audit has no unresolved major-city service failure`);
|
||||
|
||||
const counts = prefCounts(map);
|
||||
assert(counts.length >= 2 && Math.min(...counts) >= 10, `seed ${seed}: every published prefecture has at least ten municipalities`);
|
||||
assert(map.regionalDebug?.visibleCropPrefectureRepair?.minimumVisibleMunicipalities >= 10, `seed ${seed}: post-crop prefecture repair is active`);
|
||||
|
||||
const nationalLen = pathLength(map.nationalRoads);
|
||||
const railLen = pathLength([...(map.railways || []), ...(map.branchRailways || [])]);
|
||||
const ratio = nationalLen > 0 ? railLen / nationalLen : 0;
|
||||
assert(ratio >= 0.78 && ratio <= 1.00, `seed ${seed}: railway density (${ratio.toFixed(3)}) is high but remains below national-road density`);
|
||||
assert(parallelRun(map.nationalRoads || [], 3) <= 5, `seed ${seed}: no long ~1km-class national-road parallel corridor`);
|
||||
assert(parallelRun(map.expressways || [], 4) <= 5, `seed ${seed}: no long ~1km-class expressway parallel corridor`);
|
||||
assert(Math.max(0, ...(map.expressways || []).map(extremeRun)) <= 1, `seed ${seed}: expressway avoids repeated extreme bends`);
|
||||
|
||||
const trunkEdgeHits = edgeHits([...(map.nationalRoads || []), ...(map.expressways || []), ...(map.railways || []), ...(map.branchRailways || [])], map.width, map.height);
|
||||
assert(trunkEdgeHits >= 2, `seed ${seed}: hidden OD context produces real trunk continuations across published boundary`);
|
||||
}
|
||||
|
||||
const cropSource = fs.readFileSync(new URL('../src/initialGenerationCrop.js', import.meta.url), 'utf8');
|
||||
const postSource = fs.readFileSync(new URL('../src/mapPostAdminTransport.js', import.meta.url), 'utf8');
|
||||
const rendererSource = fs.readFileSync(new URL('../src/renderer.js', import.meta.url), 'utf8');
|
||||
assert(cropSource.includes('rebalanceVisiblePrefectureMunicipalityFloor(out, 10)'), 'visible prefecture floor is applied after exact crop');
|
||||
assert(cropSource.includes('pruneVisibleCropOrphanMinorRoads(out)'), 'minor-road orphan cleanup is re-run after exact crop');
|
||||
assert(postSource.includes('ensureVisibleCropMajorCityInternalService') && postSource.includes('densifyNationalToRailRatio'), 'visible-core transport quality finalizers exist');
|
||||
assert(rendererSource.includes('land fill and coastline share the exact same binary sea mask') && rendererSource.includes('color = centerWater ? waterColor : landColor'), 'coastline and land/water fill use the same binary sea mask');
|
||||
|
||||
console.log('All r11.5 visible-quality finalizer regression checks passed.');
|
||||
94
tests/r11.6-large-bestof-quality.mjs
Normal file
94
tests/r11.6-large-bestof-quality.mjs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import { generateMap } from '../src/mapPipeline.js';
|
||||
import { createWorldMap } from '../src/worldMap.js';
|
||||
import { buildMaximumProductionLasso, derivePatchSeed } from './production-fixtures.mjs';
|
||||
|
||||
const initial = generateMap(114514);
|
||||
const world = createWorldMap(initial);
|
||||
const rect = buildMaximumProductionLasso(world);
|
||||
const terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || 'auto';
|
||||
const candidatePlan = [0, 1, 2].map((variant, index) => ({
|
||||
candidateId: `r11.6-large:${variant}`,
|
||||
candidateOrdinal: index + 1,
|
||||
variant,
|
||||
seed: derivePatchSeed(world.seed, terrainType, variant),
|
||||
}));
|
||||
|
||||
const worker = new Worker(new URL('./browser-worker-node-shim.mjs', import.meta.url), { type: 'module' });
|
||||
const startedAt = performance.now();
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const timeoutMs = 540_000;
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`r11.6 max-range best-of timed out after ${timeoutMs / 1000} seconds`));
|
||||
void worker.terminate().catch(() => {});
|
||||
}, timeoutMs);
|
||||
worker.on('message', (message) => {
|
||||
if (message.id !== 1 || message.type === 'progress') return;
|
||||
clearTimeout(timer);
|
||||
if (!message.ok) reject(new Error(message.error || message.code || 'worker failed'));
|
||||
else resolve(message.result);
|
||||
});
|
||||
worker.on('error', (error) => { clearTimeout(timer); reject(error); });
|
||||
worker.postMessage({
|
||||
id: 1,
|
||||
world,
|
||||
rect,
|
||||
options: {
|
||||
patchMode: 'expansion', terrainType, variant: 0, seed: candidatePlan[0].seed,
|
||||
maxQualityRetries: 0, qualityTerrainAttempts: 1,
|
||||
acceptBestAvailableQuality: false, includeSeamVisualization: false,
|
||||
},
|
||||
search: {
|
||||
searchId: 'r11.6-large-bestof-quality', operationId: 'r11.6-large-bestof-quality',
|
||||
committedRevision: 1, workerEpoch: 1, executionAttempt: 1,
|
||||
totalCandidateCount: 3, selectBestCandidate: true, draftSelection: true,
|
||||
parallelDrafts: false, candidatePlan,
|
||||
},
|
||||
});
|
||||
});
|
||||
const elapsedMs = performance.now() - startedAt;
|
||||
|
||||
assert.equal(result?.ok, true, 'max-range best-of must produce a publishable full-production candidate');
|
||||
assert.equal(result?.bestOfCandidates, true, 'multiple-candidate highest-quality selection remains enabled');
|
||||
assert.equal(result?.candidateQuality?.hardPass, true, 'selected candidate must pass full quality');
|
||||
assert.equal(result?.candidateQuality?.finalMerge?.hardPass, true, 'selected whole-selection merge must pass');
|
||||
assert.equal(result?.seamDiagnostics?.hardPass, true, 'selected candidate must pass seam audit');
|
||||
|
||||
const attempts = result.searchAttempts || [];
|
||||
const fullAttempts = attempts.filter((a) => ['evaluated', 'success'].includes(a.status));
|
||||
assert.equal(fullAttempts.length, 3, 'overlapping admissible bounds compare all three complete candidates');
|
||||
assert.equal(result?.draftSelection?.fullGenerationPassCount, 3, 'winner publication must not perform a fourth rematerialization generation');
|
||||
const maxScore = Math.max(...fullAttempts.map((a) => Number(a.candidateQuality?.finalMerge?.score ?? -Infinity)));
|
||||
assert(Math.abs(Number(result.candidateQuality.finalMerge.score) - maxScore) < 1e-12, 'selected candidate must be the highest final-production quality among full candidates');
|
||||
assert(['admissible-bound-cached-winner', 'admissible-bound-winner-live'].includes(result?.draftSelection?.selectionReason), 'winner must publish either directly from the live exact Production state or from retained exact replay state, never by a fourth full regeneration');
|
||||
|
||||
const req = result.candidateQuality.finalMerge.transportRequirements || {};
|
||||
const cls = result.candidateQuality.finalMerge.transportClasses || {};
|
||||
for (const key of ['national', 'expressway', 'railTrunk']) {
|
||||
if (!req[key]?.demand) continue;
|
||||
assert((cls[key]?.cells || 0) >= (req[key]?.minCells || 0), `${key}: routed coverage floor must be met`);
|
||||
assert((cls[key]?.paths || 0) > 0, `${key}: demanded class must exist`);
|
||||
}
|
||||
assert(Object.entries(req).some(([key, value]) => value?.demand && (cls[key]?.paths || 0) < (value?.minPaths || 0) && (cls[key]?.cells || 0) >= (value?.minCells || 0)),
|
||||
'fixture must exercise representation-independent quality: fewer merged paths than diagnostic minPaths while routed coverage still passes');
|
||||
|
||||
const compactReq = fullAttempts.find((a) => a.candidateQuality?.finalMerge?.transportRequirements)?.candidateQuality?.finalMerge?.transportRequirements || {};
|
||||
for (const value of Object.values(compactReq)) {
|
||||
assert.equal(value.required, value.demand, 'compact Worker diagnostics must mirror the real demand field');
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
elapsedMs: Math.round(elapsedMs),
|
||||
selectedVariant: result.actualVariant,
|
||||
selectedScore: result.candidateQuality.finalMerge.score,
|
||||
scores: fullAttempts.map((a) => ({ variant: a.variant, score: a.candidateQuality?.finalMerge?.score })),
|
||||
fullGenerationPassCount: result.draftSelection.fullGenerationPassCount,
|
||||
selectionReason: result.draftSelection.selectionReason,
|
||||
transportClasses: cls,
|
||||
transportRequirements: req,
|
||||
}, null, 2));
|
||||
|
||||
await worker.terminate();
|
||||
process.exit(0);
|
||||
89
tests/r11.7-terrain-routed-transport-density.mjs
Normal file
89
tests/r11.7-terrain-routed-transport-density.mjs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
function runWorker(seed) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL('./helpers-generation-worker-node-wrapper.mjs', import.meta.url), { type: 'module' });
|
||||
const timer = setTimeout(() => { worker.terminate(); reject(new Error(`worker timeout seed ${seed}`)); }, 120000);
|
||||
worker.on('error', (error) => { clearTimeout(timer); reject(error); });
|
||||
worker.on('message', async (message) => {
|
||||
if (message?.type !== 'result' || message.id !== seed) return;
|
||||
clearTimeout(timer); worker.removeAllListeners(); await worker.terminate();
|
||||
if (!message.ok) reject(new Error(message.error || 'generation failed')); else resolve(message.map);
|
||||
});
|
||||
worker.postMessage({ id: seed, seed, options: { terrainType: 'auto' } });
|
||||
});
|
||||
}
|
||||
function pathLength(paths) {
|
||||
let sum = 0;
|
||||
for (const path of paths || []) for (let i = 1; i < (path?.length || 0); i++) sum += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]);
|
||||
return sum;
|
||||
}
|
||||
function maxGap(paths) {
|
||||
let max = 0;
|
||||
for (const path of paths || []) for (let i = 1; i < (path?.length || 0); i++) max = Math.max(max, Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]));
|
||||
return max;
|
||||
}
|
||||
function pointPathDistance(point, paths) {
|
||||
let best = Infinity;
|
||||
for (const path of paths || []) for (const q of path || []) best = Math.min(best, Math.hypot(q[0] - point.x, q[1] - point.y));
|
||||
return best;
|
||||
}
|
||||
function endpointContinuityAudit(map) {
|
||||
const bad = [];
|
||||
const exp = map.expressways || [];
|
||||
const ordinary = [...(map.nationalRoads || []), ...(map.minorRoads || []), ...(map.externalRoads || [])];
|
||||
const cities = (map.modernCities || []).filter((c) => (c.population || 0) >= 50000);
|
||||
for (let i = 0; i < exp.length; i++) {
|
||||
const path = exp[i]; if (!path?.length) continue;
|
||||
const others = exp.filter((_, j) => j !== i);
|
||||
for (const tuple of [path[0], path[path.length - 1]]) {
|
||||
const pt = { x: tuple[0], y: tuple[1] };
|
||||
const edge = pt.x < 2 || pt.y < 2 || pt.x > map.width - 3 || pt.y > map.height - 3;
|
||||
const interchange = (map.interchanges || []).some((ic) => Math.hypot(ic.x - pt.x, ic.y - pt.y) <= 6.5);
|
||||
const city = cities.some((c) => Math.hypot(c.x - pt.x, c.y - pt.y) <= 28);
|
||||
const connected = edge || pointPathDistance(pt, others) <= 3.5 || pointPathDistance(pt, ordinary) <= 4.5 || interchange || city;
|
||||
if (!connected) bad.push({ path: i, x: pt.x, y: pt.y });
|
||||
}
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(new URL('../src/mapPostAdminTransport.js', import.meta.url), 'utf8');
|
||||
assert(!source.includes('function directPath('), 'post-admin transport has no directPath straight-line fallback');
|
||||
assert(!source.includes('function directLandConnector('), 'transport has no directLandConnector straight-line fallback');
|
||||
assert(source.includes('if (gap > maxJoinGap) return []'), 'failed chain legs reject the whole trunk instead of drawing a straight chord');
|
||||
assert(source.includes('removeDiscontinuousTransportPaths(2.25)'), 'final production invariant removes sparse-jump transport paths');
|
||||
|
||||
const map = await runWorker(1);
|
||||
const trunk = {
|
||||
national: map.nationalRoads || [],
|
||||
expressway: map.expressways || [],
|
||||
rail: [...(map.railways || []), ...(map.branchRailways || [])],
|
||||
};
|
||||
for (const [name, paths] of Object.entries(trunk)) {
|
||||
assert(maxGap(paths) <= Math.SQRT2 + 1e-6, `${name}: every emitted segment is raster-contiguous; no renderer straight chord remains`);
|
||||
}
|
||||
|
||||
const nationalLength = pathLength(trunk.national);
|
||||
const railLength = pathLength(trunk.rail);
|
||||
assert(nationalLength > 0 && railLength / nationalLength >= 0.80 && railLength / nationalLength <= 1.01,
|
||||
`rail density remains high (${(railLength / nationalLength).toFixed(3)}) and approximately national-road scale`);
|
||||
|
||||
const expressLength = pathLength(trunk.expressway);
|
||||
assert((map.interchanges || []).length >= Math.max(2, Math.floor(expressLength / 18)),
|
||||
`IC density is sufficient for ${expressLength.toFixed(1)} expressway cells`);
|
||||
assert.equal(endpointContinuityAudit(map).length, 0, 'expressways have no unjustified interior dead-end endpoints');
|
||||
|
||||
const ordinary = [...(map.minorRoads || []), ...(map.nationalRoads || []), ...(map.externalRoads || [])];
|
||||
const villages = map.villages || [];
|
||||
const ruralServed = villages.filter((v) => pointPathDistance(v, ordinary) <= 4).length;
|
||||
assert(!villages.length || ruralServed / villages.length >= 0.80, `rural village road coverage is ${(ruralServed / Math.max(1, villages.length)).toFixed(3)}`);
|
||||
assert((map.minorRoads || []).length >= 60, 'published countryside retains a substantial local-road network');
|
||||
|
||||
const post = map.transportDebug?.postAdminTransportFinalization || {};
|
||||
assert((post.finalInterchangeRebuild?.added || 0) >= (post.finalInterchangeRebuild?.target || 0), 'final IC rebuild meets its production target');
|
||||
assert(post.finalDiscontinuousTransportCleanup, 'final discontinuity cleanup ran');
|
||||
|
||||
console.log('All r11.7 terrain-routed transport density regression checks passed.');
|
||||
195
tests/r11.8-terrain-topology-tooltip.mjs
Normal file
195
tests/r11.8-terrain-topology-tooltip.mjs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(HERE, '..');
|
||||
|
||||
function runWorker(seed) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL('./helpers-generation-worker-node-wrapper.mjs', import.meta.url), { type: 'module' });
|
||||
const timer = setTimeout(() => { worker.terminate(); reject(new Error(`worker timeout seed ${seed}`)); }, 180000);
|
||||
worker.on('error', (error) => { clearTimeout(timer); reject(error); });
|
||||
worker.on('message', async (message) => {
|
||||
if (message?.type !== 'result' || message.id !== seed) return;
|
||||
clearTimeout(timer);
|
||||
worker.removeAllListeners();
|
||||
await worker.terminate();
|
||||
if (!message.ok) reject(new Error(message.error || 'generation failed'));
|
||||
else resolve(message.map);
|
||||
});
|
||||
worker.postMessage({ id: seed, seed, options: { terrainType: 'auto' } });
|
||||
});
|
||||
}
|
||||
|
||||
function pathLength(paths) {
|
||||
let total = 0;
|
||||
for (const p of paths || []) for (let k = 1; k < (p?.length || 0); k++) total += Math.hypot(p[k][0] - p[k - 1][0], p[k][1] - p[k - 1][1]);
|
||||
return total;
|
||||
}
|
||||
function maxVertexGap(paths) {
|
||||
let max = 0;
|
||||
for (const p of paths || []) for (let k = 1; k < (p?.length || 0); k++) max = Math.max(max, Math.hypot(p[k][0] - p[k - 1][0], p[k][1] - p[k - 1][1]));
|
||||
return max;
|
||||
}
|
||||
function pointPathDistance(point, paths) {
|
||||
let best = Infinity;
|
||||
for (const p of paths || []) for (const q of p || []) best = Math.min(best, Math.hypot(point.x - q[0], point.y - q[1]));
|
||||
return best;
|
||||
}
|
||||
function tangent(p, k) {
|
||||
const a = p[Math.max(0, k - 2)], b = p[Math.min(p.length - 1, k + 2)];
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1], d = Math.hypot(dx, dy) || 1;
|
||||
return [dx / d, dy / d];
|
||||
}
|
||||
function longestParallelRun(paths, radius) {
|
||||
let worst = 0;
|
||||
for (let a = 0; a < (paths?.length || 0); a++) for (let b = a + 1; b < paths.length; b++) {
|
||||
let run = 0;
|
||||
for (let k = 0; k < paths[a].length; k += 2) {
|
||||
const p = paths[a][k], t = tangent(paths[a], k);
|
||||
let parallel = false;
|
||||
for (let q = 0; q < paths[b].length; q += 2) {
|
||||
const z = paths[b][q], dx = p[0] - z[0], dy = p[1] - z[1], d2 = dx * dx + dy * dy;
|
||||
if (d2 < 0.75 || d2 > radius * radius) continue;
|
||||
const u = tangent(paths[b], q);
|
||||
if (Math.abs(t[0] * u[0] + t[1] * u[1]) >= 0.90) { parallel = true; break; }
|
||||
}
|
||||
run = parallel ? run + 1 : 0;
|
||||
worst = Math.max(worst, run);
|
||||
}
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
function nearMissEndpointCount(map, source, target, maxGap) {
|
||||
let nearMiss = 0, checked = 0;
|
||||
for (const p of source || []) {
|
||||
if (!p?.length) continue;
|
||||
for (const raw of [p[0], p[p.length - 1]]) {
|
||||
const point = { x: raw[0], y: raw[1] };
|
||||
if (point.x <= 1 || point.y <= 1 || point.x >= map.width - 2 || point.y >= map.height - 2) continue;
|
||||
checked++;
|
||||
let best = Infinity;
|
||||
for (const q of target || []) {
|
||||
if (!q || q === p) continue;
|
||||
best = Math.min(best, pointPathDistance(point, [q]));
|
||||
}
|
||||
if (best > 0.75 && best <= maxGap) nearMiss++;
|
||||
}
|
||||
}
|
||||
return { nearMiss, checked, rate: nearMiss / Math.max(1, checked) };
|
||||
}
|
||||
function transportTerrainAudit(map, paths) {
|
||||
let seaCells = 0, highElevationCells = 0, extremeSlopeCells = 0, mountainRidgeCells = 0;
|
||||
for (const p of paths || []) for (const raw of p || []) {
|
||||
const x = Math.round(raw[0]), y = Math.round(raw[1]);
|
||||
if (x < 0 || y < 0 || x >= map.width || y >= map.height) { seaCells++; continue; }
|
||||
const i = y * map.width + x;
|
||||
if (map.sea?.[i]) seaCells++;
|
||||
const e = map.elevation?.[i] || 0, s = map.slope?.[i] || 0, r = map.ridgeField?.[i] || 0;
|
||||
if (e >= 0.695) highElevationCells++;
|
||||
if (s >= 0.60) extremeSlopeCells++;
|
||||
if (e >= 0.58 && r >= 0.72) mountainRidgeCells++;
|
||||
}
|
||||
return { seaCells, highElevationCells, extremeSlopeCells, mountainRidgeCells };
|
||||
}
|
||||
function expresswayDeadEnds(map) {
|
||||
const main = map.expressways || [], external = map.externalExpressways || [];
|
||||
const bad = [];
|
||||
for (let i = 0; i < main.length; i++) {
|
||||
const path = main[i]; if (!path?.length) continue;
|
||||
for (const raw of [path[0], path[path.length - 1]]) {
|
||||
const p = { x: raw[0], y: raw[1] };
|
||||
if (p.x <= 2 || p.y <= 2 || p.x >= map.width - 3 || p.y >= map.height - 3) continue;
|
||||
const joined = pointPathDistance(p, [...main.filter((_, j) => j !== i), ...external]) <= 3.5;
|
||||
const ic = (map.interchanges || []).some((q) => Math.hypot(q.x - p.x, q.y - p.y) <= 9.0);
|
||||
const city = (map.modernCities || []).some((q) => (q.population || 0) >= 50000 && Math.hypot(q.x - p.x, q.y - p.y) <= 28);
|
||||
const port = (map.ports || []).some((q) => Math.hypot(q.x - p.x, q.y - p.y) <= 18);
|
||||
if (!(joined || (ic && (city || port)))) bad.push({ x: p.x, y: p.y, joined, ic, city, port });
|
||||
}
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
for (const seed of [1, 2]) {
|
||||
const map = await runWorker(seed);
|
||||
const national = map.nationalRoads || [];
|
||||
const expressway = map.expressways || [];
|
||||
const rail = [...(map.railways || []), ...(map.branchRailways || [])];
|
||||
const local = map.minorRoads || [];
|
||||
const trunk = [...national, ...expressway, ...rail];
|
||||
|
||||
assert(maxVertexGap(trunk) <= Math.SQRT2 + 1e-6, `seed ${seed}: all emitted trunk geometry is raster-contiguous`);
|
||||
const terrain = transportTerrainAudit(map, trunk);
|
||||
assert.equal(terrain.seaCells, 0, `seed ${seed}: no published trunk cell crosses sea`);
|
||||
assert.equal(terrain.highElevationCells, 0, `seed ${seed}: no published trunk cell crosses the hard high-elevation ceiling`);
|
||||
assert.equal(terrain.extremeSlopeCells, 0, `seed ${seed}: no published trunk cell crosses an extreme slope`);
|
||||
assert.equal(terrain.mountainRidgeCells, 0, `seed ${seed}: no published trunk cell traverses a high mountain ridge`);
|
||||
|
||||
assert(longestParallelRun(national, 3) <= 5, `seed ${seed}: national-road kilometre-scale parallelism is bounded`);
|
||||
assert(longestParallelRun(expressway, 4) <= 3, `seed ${seed}: expressway parallel corridors are collapsed`);
|
||||
|
||||
const nationalMiss = nearMissEndpointCount(map, national, [...national, ...(map.externalRoads || [])], 4.6);
|
||||
const expressMiss = nearMissEndpointCount(map, expressway, [...expressway, ...(map.externalExpressways || [])], 5.0);
|
||||
const railMiss = nearMissEndpointCount(map, rail, [...rail, ...(map.externalRailways || [])], 4.5);
|
||||
const localMiss = nearMissEndpointCount(map, local, [...local, ...national, ...(map.externalRoads || [])], 3.6);
|
||||
assert(nationalMiss.nearMiss <= 3, `seed ${seed}: national near-miss endpoints are rare (${nationalMiss.nearMiss})`);
|
||||
assert.equal(expressMiss.nearMiss, 0, `seed ${seed}: expressways have no close-but-unjoined endpoints`);
|
||||
assert.equal(railMiss.nearMiss, 0, `seed ${seed}: railways have no close-but-unjoined endpoints`);
|
||||
assert(localMiss.rate <= 0.015, `seed ${seed}: local close-but-unjoined endpoint rate is ${(localMiss.rate * 100).toFixed(2)}%`);
|
||||
|
||||
const nationalLen = pathLength(national), expressLen = pathLength(expressway), railLen = pathLength(rail);
|
||||
assert(nationalLen > 0 && railLen / nationalLen >= 0.82 && railLen / nationalLen <= 1.02,
|
||||
`seed ${seed}: rail density remains high and slightly below national-road scale (${(railLen / nationalLen).toFixed(3)})`);
|
||||
const icCount = (map.interchanges || []).length;
|
||||
assert(icCount <= Math.ceil(expressLen / 12) + 2, `seed ${seed}: IC count is not over-dense (${icCount} for ${expressLen.toFixed(1)} cells)`);
|
||||
assert(icCount >= Math.max(1, Math.floor(expressLen / 30)), `seed ${seed}: retained expressway still has usable IC coverage`);
|
||||
assert.equal(expresswayDeadEnds(map).length, 0, `seed ${seed}: internal expressway endpoints are connected or terminate at a city/port IC`);
|
||||
|
||||
const ordinary = [...local, ...national, ...(map.externalRoads || [])];
|
||||
const villages = map.villages || [];
|
||||
const ruralServed = villages.filter((v) => pointPathDistance(v, ordinary) <= 4).length;
|
||||
assert(!villages.length || ruralServed / villages.length >= 0.80, `seed ${seed}: rural-road village coverage is ${(ruralServed / Math.max(1, villages.length)).toFixed(3)}`);
|
||||
assert(local.length >= 70, `seed ${seed}: countryside retains a substantial organic local-road network (${local.length})`);
|
||||
|
||||
const post = map.transportDebug?.postAdminTransportFinalization || {};
|
||||
assert.equal(post.visibleCropMajorCityService?.missing?.length || 0, 0, `seed ${seed}: visible major-city trunk service has no unresolved city`);
|
||||
assert.equal(post.postDedupeMajorCityService?.missing?.length || 0, 0, `seed ${seed}: final major-city national/rail/expressway contract passes`);
|
||||
assert(post.absoluteFinalTerrainInvariant, `seed ${seed}: absolute final terrain invariant executed`);
|
||||
const interchangeAudit = post.absoluteFinalInterchangeRebuild;
|
||||
assert(interchangeAudit, `seed ${seed}: final IC rebuild is audited`);
|
||||
assert(interchangeAudit.added >= icCount, `seed ${seed}: visible ICs are retained from the rebuilt pre-crop IC set`);
|
||||
assert.equal(interchangeAudit.legacyUniformTarget, Math.max(0, Math.round(interchangeAudit.expresswayLength / 19.5)), `seed ${seed}: IC density reference is derived from the audited expressway length`);
|
||||
assert(interchangeAudit.target >= interchangeAudit.added, `seed ${seed}: IC target accounts for every published IC`);
|
||||
}
|
||||
|
||||
const featureSource = fs.readFileSync(path.join(ROOT, 'src/mapFeatures.js'), 'utf8');
|
||||
const postSource = fs.readFileSync(path.join(ROOT, 'src/mapPostAdminTransport.js'), 'utf8');
|
||||
const cropSource = fs.readFileSync(path.join(ROOT, 'src/initialGenerationCrop.js'), 'utf8');
|
||||
const rendererSource = fs.readFileSync(path.join(ROOT, 'src/renderer.js'), 'utf8');
|
||||
const appSource = fs.readFileSync(path.join(ROOT, 'src/app.js'), 'utf8');
|
||||
const cssSource = fs.readFileSync(path.join(ROOT, 'styles/styles.css'), 'utf8');
|
||||
|
||||
assert(!postSource.includes('function directPath(') && !postSource.includes('function directLandConnector('), 'no direct trunk fallback exists');
|
||||
assert(featureSource.includes('const trunkMode = mode === "expressway" || mode === "national" || mode === "rail"'), 'all production trunk route call-sites share one terrain-first policy clamp');
|
||||
assert(featureSource.includes('heuristicWeight: trunkMode') && featureSource.includes('Math.min(options.heuristicWeight ?? 0.08'), 'late trunk callers cannot restore a dominant Euclidean heuristic');
|
||||
assert(featureSource.includes('literal endpoint chord') && postSource.includes('geometric chord'), 'subtle near-straight terrain-blind corridors are explicitly audited, not just coordinate jumps');
|
||||
assert(postSource.includes('if (direct >= 18 && chordHardShare >= 0.08 && straightness > 0.90) return false'), 'final production terrain validator rejects near-chord trunks when the chord crosses hostile terrain');
|
||||
assert(postSource.includes('runs.maxSeaRun > 0'), 'shared trunk alignment cannot reintroduce a sea crossing');
|
||||
assert(cropSource.includes('let alreadyConnected = false') && cropSource.includes('passes: 2'), 'visible-crop road welding distinguishes exact junctions from near misses and performs a second bounded pass');
|
||||
assert(postSource.includes('if (!hit || hit.connected) continue'), 'post-admin topology repair does not keep extending already-connected endpoints');
|
||||
assert(postSource.includes('if (total < 8)') && postSource.includes('final-terminal-ic'), 'retained short expressway spurs still receive a real terminal IC rather than ending mid-road');
|
||||
|
||||
const municipalDraw = rendererSource.indexOf('drawLabels(ctx, municipalFallbackLabels, Infinity, occupiedLabels)');
|
||||
const prefectureDraw = rendererSource.indexOf('drawLabels(ctx, prefectureLabels, Infinity, occupiedLabels)', municipalDraw + 1);
|
||||
assert(municipalDraw >= 0 && prefectureDraw > municipalDraw, 'prefecture labels render above municipality labels in the general map pass');
|
||||
assert(rendererSource.includes('drawLabels(ctx, prefectureLabels, Infinity, adminOccupied)'), 'prefecture labels also render last in administrative mode');
|
||||
assert(rendererSource.includes('land fill and coastline share the exact same binary sea mask'), 'coastline and land fill retain their shared binary sea-mask contract');
|
||||
|
||||
assert(cssSource.includes('background:rgba(17,22,31,.84)'), 'tooltip is slightly translucent');
|
||||
assert(appSource.includes('cursorY + 10') && appSource.includes('maxTop'), 'tooltip follows the cursor to the bottom before clamping');
|
||||
assert(appSource.includes('const exclusion = 12') && appSource.includes('leftAlt') && appSource.includes('rightAlt'), 'tooltip reserves a cursor exclusion zone and flips horizontally at edges');
|
||||
|
||||
console.log('All r11.8 terrain/topology/tooltip regression checks passed.');
|
||||
|
|
@ -209,7 +209,7 @@ if (launchBrowser) {
|
|||
const server = createServer(async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.url || "/", "http://127.0.0.1");
|
||||
const relative = decodeURIComponent(url.pathname === "/" ? "/tests/additional-generation-e2e.html" : url.pathname);
|
||||
const relative = decodeURIComponent(url.pathname === "/" ? "/index.html" : url.pathname);
|
||||
const file = resolve(workspace, `.${relative}`);
|
||||
if (file !== workspace && !file.startsWith(`${workspace}${sep}`)) throw new Error("Path outside workspace");
|
||||
const bytes = await readFile(file);
|
||||
|
|
|
|||
|
|
@ -2,54 +2,70 @@ import { performance } from "node:perf_hooks";
|
|||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const defaultSuites = [
|
||||
"additional-generation-unit",
|
||||
"additional-generation-coverage-worker",
|
||||
"core",
|
||||
"terrain",
|
||||
"terrain-name",
|
||||
"admin",
|
||||
"patch",
|
||||
"patch-large",
|
||||
"determinism-114514",
|
||||
"determinism-12345",
|
||||
"determinism-54321",
|
||||
"determinism-777",
|
||||
"determinism-999",
|
||||
const cwd = fileURLToPath(new URL(".", import.meta.url));
|
||||
const testFile = fileURLToPath(new URL("./test.js", import.meta.url));
|
||||
const suiteDefinitions = [
|
||||
{ name: "additional-generation-unit", file: "./additional-generation-unit.mjs", timeoutMs: 180_000, canonical: true },
|
||||
{ name: "additional-generation-coverage-worker", file: "./additional-generation-coverage-worker.mjs", timeoutMs: 240_000, canonical: true },
|
||||
{ name: "r10-exact-production-worker", file: "./r10-exact-production-worker.mjs", timeoutMs: 180_000, canonical: true },
|
||||
{ name: "r11-selection-native-production", file: "./r11-selection-native-production.mjs", timeoutMs: 180_000, canonical: true },
|
||||
{ name: "additional-generation-max-worker", file: "./additional-generation-max-worker.mjs", timeoutMs: 240_000, release: true },
|
||||
{ name: "patch-worker-cancel", file: "./patch-worker-cancel.mjs", timeoutMs: 180_000, release: true },
|
||||
{ name: "patch-worker-mirror-sync", file: "./patch-worker-mirror-sync.mjs", timeoutMs: 180_000, release: true },
|
||||
{ name: "r11.4-literal-initial-overscan-worker", file: "./r11.4-literal-initial-overscan-worker.mjs", timeoutMs: 300_000, release: true },
|
||||
{ name: "r11.4-transport-demography-overscan", file: "./r11.4-transport-demography-overscan.mjs", timeoutMs: 300_000, release: true },
|
||||
{ name: "r11.5-visible-quality-finalizer", file: "./r11.5-visible-quality-finalizer.mjs", timeoutMs: 600_000, release: true },
|
||||
{ name: "r11.6-large-bestof-quality", file: "./r11.6-large-bestof-quality.mjs", timeoutMs: 600_000, release: true },
|
||||
{ name: "r11.7-terrain-routed-transport-density", file: "./r11.7-terrain-routed-transport-density.mjs", timeoutMs: 300_000, release: true },
|
||||
{ name: "r11.8-terrain-topology-tooltip", file: "./r11.8-terrain-topology-tooltip.mjs", timeoutMs: 600_000, release: true },
|
||||
{ name: "additional-generation-browser", file: "./run-additional-generation-browser.mjs", timeoutMs: 600_000, browser: true },
|
||||
{ name: "core", timeoutMs: 360_000, canonical: true },
|
||||
{ name: "terrain", timeoutMs: 600_000, canonical: true },
|
||||
{ name: "terrain-name", timeoutMs: 240_000, canonical: true },
|
||||
{ name: "admin", timeoutMs: 300_000, canonical: true },
|
||||
{ name: "patch", timeoutMs: 300_000, canonical: true },
|
||||
{ name: "patch-large", timeoutMs: 600_000, canonical: true },
|
||||
{ name: "determinism-114514", timeoutMs: 240_000, canonical: true },
|
||||
{ name: "determinism-12345", timeoutMs: 240_000, canonical: true },
|
||||
{ name: "determinism-54321", timeoutMs: 240_000, canonical: true },
|
||||
{ name: "determinism-777", timeoutMs: 240_000, canonical: true },
|
||||
{ name: "determinism", timeoutMs: 240_000 },
|
||||
{ name: "all", timeoutMs: 900_000 },
|
||||
];
|
||||
const suiteRegistry = new Map(suiteDefinitions.map((definition) => [definition.name, definition]));
|
||||
const suiteGroups = new Map([
|
||||
["default", suiteDefinitions.filter((definition) => definition.canonical).map((definition) => definition.name)],
|
||||
["release", suiteDefinitions.filter((definition) => definition.canonical || definition.release).map((definition) => definition.name)],
|
||||
["browser", suiteDefinitions.filter((definition) => definition.browser).map((definition) => definition.name)],
|
||||
]);
|
||||
const defaultTimeoutMs = 240_000;
|
||||
|
||||
function resolveSuite(name) {
|
||||
const registered = suiteRegistry.get(name);
|
||||
if (registered) return registered;
|
||||
if (/^determinism-(?:0|[1-9]\d*)$/.test(name)) return { name, timeoutMs: defaultTimeoutMs };
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestedSuites = String(process.env.TEST_SUITES || "").split(",").map((value) => value.trim()).filter(Boolean);
|
||||
const suites = requestedSuites.length ? requestedSuites : defaultSuites;
|
||||
const requestedGroup = String(process.env.TEST_GROUP || "default").trim();
|
||||
if (!suiteGroups.has(requestedGroup)) throw new Error(`Unknown test group: ${requestedGroup}`);
|
||||
const suites = requestedSuites.length ? requestedSuites : suiteGroups.get(requestedGroup);
|
||||
const unknownSuites = suites.filter((suite) => !resolveSuite(suite));
|
||||
if (unknownSuites.length) throw new Error(`Unknown test suite${unknownSuites.length === 1 ? "" : "s"}: ${unknownSuites.join(", ")}`);
|
||||
const concurrency = Math.max(1, Math.min(2, Number(process.env.TEST_CONCURRENCY) || 1));
|
||||
// Full-map shards can briefly peak at several hundred MB. CI may opt into a
|
||||
// handoff delay when its runtime needs extra time to reclaim a completed child.
|
||||
const suiteCooldownMs = Math.max(0, Number(process.env.TEST_SUITE_COOLDOWN_MS ?? 0));
|
||||
const suiteTimeoutMs = {
|
||||
"additional-generation-unit": 180_000,
|
||||
"additional-generation-coverage-worker": 120_000,
|
||||
core: 360_000,
|
||||
terrain: 600_000,
|
||||
"terrain-name": 240_000,
|
||||
admin: 300_000,
|
||||
patch: 300_000,
|
||||
"patch-large": 600_000,
|
||||
};
|
||||
const defaultTimeoutMs = 240_000;
|
||||
const maxOutputBytes = 32 * 1024 * 1024;
|
||||
const cwd = fileURLToPath(new URL(".", import.meta.url));
|
||||
const testFile = fileURLToPath(new URL("./test.js", import.meta.url));
|
||||
const additionalUnitFile = fileURLToPath(new URL("./additional-generation-unit.mjs", import.meta.url));
|
||||
const additionalCoverageWorkerFile = fileURLToPath(new URL("./additional-generation-coverage-worker.mjs", import.meta.url));
|
||||
|
||||
function runSuite(suite) {
|
||||
return new Promise((resolve) => {
|
||||
const timeoutMs = suiteTimeoutMs[suite] || defaultTimeoutMs;
|
||||
const descriptor = resolveSuite(suite);
|
||||
const timeoutMs = descriptor.timeoutMs || defaultTimeoutMs;
|
||||
const started = performance.now();
|
||||
console.error(`[test-all] start ${suite}`);
|
||||
const standaloneFile = suite === "additional-generation-unit"
|
||||
? additionalUnitFile
|
||||
: suite === "additional-generation-coverage-worker"
|
||||
? additionalCoverageWorkerFile
|
||||
: null;
|
||||
const standaloneFile = descriptor.file ? fileURLToPath(new URL(descriptor.file, import.meta.url)) : null;
|
||||
const commandFile = standaloneFile || testFile;
|
||||
const commandArgs = standaloneFile ? [commandFile] : [commandFile, `--suite=${suite}`];
|
||||
const child = spawn(process.execPath, commandArgs, {
|
||||
|
|
@ -57,25 +73,33 @@ function runSuite(suite) {
|
|||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const stdoutChunks = [];
|
||||
const stderrChunks = [];
|
||||
let outputBytes = 0;
|
||||
let outputOverflow = false;
|
||||
let timedOut = false;
|
||||
let forceKillTimer = null;
|
||||
let resolved = false;
|
||||
|
||||
const append = (current, chunk) => {
|
||||
if (outputOverflow) return current;
|
||||
const next = current + chunk.toString("utf8");
|
||||
if (Buffer.byteLength(next, "utf8") > maxOutputBytes) {
|
||||
const finish = (payload) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
resolve(payload);
|
||||
};
|
||||
const append = (target, chunk) => {
|
||||
if (outputOverflow) return;
|
||||
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
if (outputBytes + buffer.byteLength > maxOutputBytes) {
|
||||
outputOverflow = true;
|
||||
child.kill("SIGTERM");
|
||||
return current;
|
||||
return;
|
||||
}
|
||||
return next;
|
||||
outputBytes += buffer.byteLength;
|
||||
target.push(buffer);
|
||||
};
|
||||
|
||||
child.stdout.on("data", (chunk) => { stdout = append(stdout, chunk); });
|
||||
child.stderr.on("data", (chunk) => { stderr = append(stderr, chunk); });
|
||||
child.stdout.on("data", (chunk) => append(stdoutChunks, chunk));
|
||||
child.stderr.on("data", (chunk) => append(stderrChunks, chunk));
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
|
|
@ -90,7 +114,7 @@ function runSuite(suite) {
|
|||
if (forceKillTimer) clearTimeout(forceKillTimer);
|
||||
const seconds = Math.round((performance.now() - started) / 10) / 100;
|
||||
console.error(`[test-all] error ${suite}: ${error.message}`);
|
||||
resolve({
|
||||
finish({
|
||||
suite,
|
||||
seconds,
|
||||
fullMapGenerations: null,
|
||||
|
|
@ -107,22 +131,31 @@ function runSuite(suite) {
|
|||
clearTimeout(timeout);
|
||||
if (forceKillTimer) clearTimeout(forceKillTimer);
|
||||
const seconds = Math.round((performance.now() - started) / 10) / 100;
|
||||
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
||||
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
||||
const output = `${stdout}\n${stderr}`;
|
||||
const ng = (output.match(/^NG:/gm) || []).length;
|
||||
const info = output.match(/INFO: suite=([^;]+); fullMapGenerations=(\d+); elapsedMs=(\d+)/);
|
||||
const infrastructureError = outputOverflow
|
||||
? `Output exceeded ${maxOutputBytes} bytes`
|
||||
: null;
|
||||
const testFailure = status !== 0 && !signal && !timedOut && !outputOverflow && !infrastructureError;
|
||||
const failedAssertions = ng > 0 ? ng : testFailure ? 1 : 0;
|
||||
console.error(
|
||||
`[test-all] end ${suite}: status=${status} signal=${signal || "none"} ` +
|
||||
`failures=${ng} seconds=${seconds}`,
|
||||
`failures=${failedAssertions} seconds=${seconds}`,
|
||||
);
|
||||
resolve({
|
||||
if (testFailure || signal || timedOut || outputOverflow || infrastructureError) {
|
||||
const tail = output.trim().slice(-16_000);
|
||||
if (tail) console.error(`[test-all] output ${suite}:\n${tail}`);
|
||||
}
|
||||
finish({
|
||||
suite,
|
||||
seconds,
|
||||
fullMapGenerations: info ? Number(info[2]) : null,
|
||||
elapsedMsReported: info ? Number(info[3]) : null,
|
||||
failedAssertions: ng,
|
||||
failedAssertions,
|
||||
testFailure,
|
||||
status,
|
||||
signal: signal || null,
|
||||
timedOut,
|
||||
|
|
@ -157,7 +190,7 @@ const failures = completed.reduce(
|
|||
0,
|
||||
);
|
||||
const infrastructureFailure = completed.some(
|
||||
(row) => row.status !== 0 || row.signal || row.timedOut || row.outputOverflow || row.infrastructureError,
|
||||
(row) => row.signal || row.timedOut || row.outputOverflow || row.infrastructureError,
|
||||
);
|
||||
const withinSuiteBudgets = completed.every((row) => !row.timedOut);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import { generateMap } from "../src/mapPipeline.js";
|
||||
const seed = Number(process.argv[2]);
|
||||
if (!Number.isFinite(seed)) throw new Error("seed is required");
|
||||
const a = generateMap(seed, { onProgress() {} });
|
||||
const b = generateMap(seed, { onProgress() {} });
|
||||
console.log(JSON.stringify({
|
||||
seed,
|
||||
adminIdEqual: Buffer.from(a.adminId.buffer, a.adminId.byteOffset, a.adminId.byteLength).equals(Buffer.from(b.adminId.buffer, b.adminId.byteOffset, b.adminId.byteLength)),
|
||||
adminDebugEqual: JSON.stringify(a.adminDebug) === JSON.stringify(b.adminDebug),
|
||||
}));
|
||||
166
tests/test.js
166
tests/test.js
|
|
@ -22,6 +22,10 @@ const TEST_SUITE = (() => {
|
|||
const arg = process.argv.find((value) => value.startsWith("--suite="));
|
||||
return arg ? arg.slice("--suite=".length) : "core";
|
||||
})();
|
||||
const STATIC_TEST_SUITES = new Set(["all", "core", "terrain", "terrain-name", "admin", "patch", "patch-large", "determinism"]);
|
||||
if (!STATIC_TEST_SUITES.has(TEST_SUITE) && !/^determinism-(?:0|[1-9]\d*)$/.test(TEST_SUITE)) {
|
||||
throw new Error(`Unknown test suite: ${TEST_SUITE}`);
|
||||
}
|
||||
const TEST_STARTED_AT = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
||||
const DETERMINISM_SEED = (() => {
|
||||
if (IS_BROWSER) return Number(new URLSearchParams(location.search).get("seed")) || 114514;
|
||||
|
|
@ -43,22 +47,36 @@ async function readLocalText(path) {
|
|||
return readFile(new URL(path, import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
const [namesSource, mapGeneratorSource, mapOutputSource, mapTerrainSource, rendererSource, appSource, mapPipelineSource, mapAdminStageSource, mapPatchSource, mapPatchWorkerSource, committedWorldDeltaSource, worldMapSource, municipalSource, testSource] = await Promise.all([
|
||||
readLocalText("../src/names.js"),
|
||||
readLocalText("../src/mapPipeline.js"),
|
||||
readLocalText("../src/mapOutput.js"),
|
||||
readLocalText("../src/mapTerrain.js"),
|
||||
readLocalText("../src/renderer.js"),
|
||||
readLocalText("../src/app.js"),
|
||||
readLocalText("../src/mapPipeline.js"),
|
||||
readLocalText("../src/mapAdminStage.js"),
|
||||
readLocalText("../src/mapPatch.js"),
|
||||
readLocalText("../src/mapPatchWorker.js"),
|
||||
readLocalText("../src/committedWorldDelta.js"),
|
||||
readLocalText("../src/worldMap.js"),
|
||||
readLocalText("../src/mapMunicipalCoherence.js"),
|
||||
readLocalText("./test.js"),
|
||||
]);
|
||||
let namesSource = "";
|
||||
let mapGeneratorSource = "";
|
||||
let mapOutputSource = "";
|
||||
let mapTerrainSource = "";
|
||||
let rendererSource = "";
|
||||
let appSource = "";
|
||||
let mapPipelineSource = "";
|
||||
let mapAdminStageSource = "";
|
||||
let mapPatchSource = "";
|
||||
let mapPatchWorkerSource = "";
|
||||
let committedWorldDeltaSource = "";
|
||||
let worldMapSource = "";
|
||||
let municipalSource = "";
|
||||
if (suiteEnabled("core")) {
|
||||
[namesSource, mapOutputSource, mapTerrainSource, rendererSource, appSource, mapPipelineSource, mapAdminStageSource, mapPatchSource, mapPatchWorkerSource, committedWorldDeltaSource, worldMapSource, municipalSource] = await Promise.all([
|
||||
readLocalText("../src/names.js"),
|
||||
readLocalText("../src/mapOutput.js"),
|
||||
readLocalText("../src/mapTerrain.js"),
|
||||
readLocalText("../src/renderer.js"),
|
||||
readLocalText("../src/app.js"),
|
||||
readLocalText("../src/mapPipeline.js"),
|
||||
readLocalText("../src/mapAdminStage.js"),
|
||||
readLocalText("../src/mapPatch.js"),
|
||||
readLocalText("../src/mapPatchWorker.js"),
|
||||
readLocalText("../src/committedWorldDelta.js"),
|
||||
readLocalText("../src/worldMap.js"),
|
||||
readLocalText("../src/mapMunicipalCoherence.js"),
|
||||
]);
|
||||
mapGeneratorSource = mapPipelineSource;
|
||||
}
|
||||
const derivePatchSeedStart = appSource.indexOf("function derivePatchSeed");
|
||||
const derivePatchSeedEnd = derivePatchSeedStart >= 0 ? appSource.indexOf("\n}", derivePatchSeedStart) : -1;
|
||||
const derivePatchSeedSource = derivePatchSeedStart >= 0 && derivePatchSeedEnd > derivePatchSeedStart
|
||||
|
|
@ -707,7 +725,6 @@ try {
|
|||
let terrainSeedSummaries = [];
|
||||
if (suiteEnabled("core")) {
|
||||
const map = generateTestMap(12345);
|
||||
const other = generateTestMap(54321);
|
||||
const urbanCellCount = [...map.landuse].filter((value) => value >= 2 && value <= 8).length;
|
||||
const cityPopulations = map.modernCities.map((city) => city.population || 0);
|
||||
const maxPopulation = Math.max(...cityPopulations);
|
||||
|
|
@ -827,7 +844,7 @@ try {
|
|||
assert(NAME_TEMPLATE_WEIGHTS && NAME_TEMPLATE_WEIGHTS.generic?.modifierTerrain > 0, "NAME_TEMPLATE_WEIGHTS exists");
|
||||
assert(NAME_PROBABILITIES && NAME_PROBABILITIES.contextCategoryWeights?.generic, "NAME_PROBABILITIES exists");
|
||||
const removedContextModule = "placeName" + "Context.js";
|
||||
assert(!namesSource.includes(removedContextModule) && !mapGeneratorSource.includes(removedContextModule) && !testSource.includes(removedContextModule), "removed name-context import is absent");
|
||||
assert(!namesSource.includes(removedContextModule) && !mapGeneratorSource.includes(removedContextModule), "removed name-context import is absent from production modules");
|
||||
assert(Object.keys(NAME_KANJI_POOLS).every((key) => Array.isArray(NAME_KANJI_POOLS[key])), "name category pools are centralized arrays");
|
||||
assert(Object.values(NAME_KANJI_POOLS).every((pool) => pool.every((part) => typeof part === "string" && !part.includes("\uFFFD"))), "configured name category pools contain valid strings");
|
||||
const removedContextSuffixConst = "CONTEXT" + "_SUFFIXES";
|
||||
|
|
@ -837,6 +854,18 @@ try {
|
|||
assert(mapPatchSource.includes("splitWorldPathByPatch") && mapPatchSource.includes("patchAffected"), "patch path merging is alpha-aware for lasso selections");
|
||||
assert(!mapPatchSource.includes("patchAlpha(x, y, rects, 0)"), "patch admin repair uses the active patch seed");
|
||||
assert(mapPatchSource.includes("refreshPatchInfluenceFields") && mapPatchSource.includes("roadCellsPainted"), "patch generation refreshes derived transport influence fields after path merges");
|
||||
assert(mapPatchSource.includes("createTransportPathSpatialIndex") && mapPatchSource.includes("TRANSPORT_SPATIAL_BUCKET_SIZE"), "patch transport queries use a bucketed spatial index instead of repeatedly scanning every path");
|
||||
assert(mapPatchSource.includes("pathfindScratch") && mapPatchSource.includes("Float32Array") && mapPatchSource.includes("buildHierarchicalPathCorridor"), "patch connector pathfinding reuses typed scratch storage and has a coarse-to-fine hierarchy");
|
||||
assert(mapPatchSource.includes("TransportUnionFind") && mapPatchSource.includes("GraphIncrementalUnions") && mapPatchSource.includes("GraphFullRebuilds"), "transport graph repair tracks connector merges incrementally and reserves full rebuilds for audit");
|
||||
assert(mapPatchSource.includes("getRadialInfluenceKernel") && mapPatchSource.includes("paintInfluenceDisk"), "regional influence refresh reuses exact radial kernels instead of recalculating distance powers per painted cell");
|
||||
assert(mapPatchSource.includes("regionalUrbanScratch") && mapPatchSource.includes("population.subarray"), "regional urban recalculation reuses compact scratch buffers and row copies");
|
||||
assert(mapPatchSource.includes("ensurePatchSelectionDistanceCache") && mapPatchSource.includes("_selectionDistanceCache") && mapPatchSource.includes("selectionDistanceCacheReused"), "lasso regional transport and urban recalculation share one lazy selection-distance raster");
|
||||
assert(mapPatchWorkerSource.includes("admissible-terrain-scout-branch-and-bound-two-lane-v2") && mapPatchWorkerSource.includes("draftCandidateUpperBound") && mapPatchWorkerSource.includes("branchBoundPrunedCount") && mapPatchWorkerSource.includes("precomputeRawTerrainScoutBatch") && mapPatchWorkerSource.includes("fullProductionFromTerrainOnly") && !mapPatchWorkerSource.includes("draftNearTieFullGap"), "candidate ranking uses admissible Branch-and-Bound with resident two-lane terrain scouts; simplified human/transport drafts are never reused for publication");
|
||||
assert(worldMapSource.includes("INITIAL_QUALITY_TRANSPORT_CLASSES") && worldMapSource.includes("initial-production-quality-v2") && mapPatchSource.includes("transportHierarchyPass"), "initial quality oracle records national, expressway, and rail-trunk production density and enforces hierarchy-aware final quality");
|
||||
assert(mapPatchSource.includes("finalizeRegionalTrunkTransport") && mapPatchSource.includes("patchRegionalTrunkFinalizer") && mapPatchSource.includes("productionTransportParity"), "full additional-generation finalists restore production trunk transport and fill missing national, expressway, and trunk-rail demand");
|
||||
assert(mapPatchSource.includes("connectorLayerForEndpoints") && mapPatchSource.includes("transportHierarchyAtPoint"), "transport seam repair preserves expressway, national-road, and trunk-rail hierarchy instead of demoting every connector");
|
||||
assert(appSource.includes("PATCH_SEARCH_BATCH_SIZE = 3") && appSource.includes("PATCH_SEARCH_DEFAULT_LIMIT = 12") && appSource.includes("contentBatchExhausted") && appSource.includes("allCandidatePlan"), "quality-rejected candidates advance automatically in three-candidate batches up to twelve without publishing drafts");
|
||||
assert(!appSource.includes("BEST AVAILABLE") && appSource.includes("acceptBestAvailableQuality: false"), "top-level preview publication has no best-available quality bypass");
|
||||
assert(mapPatchSource.includes("normalizeGeneratedPointIds"), "patch generation normalizes generated point admin and prefecture ids");
|
||||
assert(mapPatchSource.includes("generateUnifiedWorldNativePatchCandidate") && mapPatchSource.includes("generateMap(seed") && mapPatchSource.includes("unified-world-native-patch"), "patch modes execute the complete production generation pipeline");
|
||||
assert(!mapPatchSource.includes("generateVariablePatchCandidate") && !mapPatchSource.includes("PATCH_VARIABLE_CANDIDATE_ENABLED"), "retired variable rectangle candidate implementation is removed");
|
||||
|
|
@ -848,7 +877,7 @@ try {
|
|||
assert(derivePatchSeedSource.includes("function derivePatchSeed(world, terrainType, variant") && !derivePatchSeedSource.includes("rect.x") && !derivePatchSeedSource.includes("rect.y"), "UI patch seed is independent of selection bounds and backing-world padding");
|
||||
assert(!appSource.includes("qualityWorkerRetries: 1") && !appSource.includes("attemptVariant = (attemptVariant + 3)"), "UI does not run the obsolete hidden whole-patch retry wrapper");
|
||||
assert(mapPatchSource.includes("generateTiledRegenerationPatch") && mapPatchSource.includes("patch-candidate-coverage-incomplete"), "large Regeneration is tiled and rejects uncovered active cells instead of silently skipping them");
|
||||
assert(mapPatchSource.includes("single-explicit-production-candidate-v2") && mapPatchSource.includes("const requestedAttempts = 1"), "each search candidate runs exactly one explicit complete production variant");
|
||||
assert(mapPatchSource.includes("initial-quality-oracle-admin-transport-coherence-v4") && mapPatchSource.includes("const requestedAttempts = 1"), "each search candidate runs exactly one explicit complete production variant");
|
||||
assert(mapPatchWorkerSource.includes("runPatchCandidateSearch") && mapPatchWorkerSource.includes("candidatePlan") && mapPatchWorkerSource.includes("patch-search-exhausted"), "worker owns a bounded multi-candidate search controller");
|
||||
assert(mapPatchWorkerSource.includes("persistentCommittedMirror") && appSource.includes("reuseCommittedMirror") && appSource.includes("committedRevision"), "warm Alternative searches reuse a revision-checked committed Worker mirror");
|
||||
assert(mapPatchWorkerSource.includes("patch-apply-ack") && appSource.includes("acknowledgePatchApply"), "Apply advances the persistent mirror through a revision-checked transactional ACK");
|
||||
|
|
@ -1051,10 +1080,19 @@ try {
|
|||
assert(worldMapSource.includes("seaLevel: Number.isFinite(initialMap?.seaLevel)"), "world map persists the initial sea level as a world invariant");
|
||||
assert(mapPatchSource.includes("capturePatchSeamSnapshot") && mapPatchSource.includes("analyzePatchSeam") && mapPatchSource.includes("roadPortalsBroken") && mapPatchSource.includes("duplicateBoundaryPairs"), "patch generation records coast, transport, and boundary seam diagnostics");
|
||||
assert(appSource.includes("advancedSeamDiagnostics") && appSource.includes("showSeamDiagnostics") && appSource.includes("seamDiagnosticRows"), "seam diagnostics are exposed in the UI and map overlay controls");
|
||||
assert(appSource.includes("state.world.sourceMap.patchSeamDiagnostics.enabled = false")
|
||||
&& appSource.includes("state.showSeamDiagnostics = false")
|
||||
&& appSource.includes("showSeamDiagnosticsInput.checked = false"),
|
||||
"applying an additional-generation preview disables and unchecks the seam diagnostic overlay so red dotted diagnostics cannot become stuck");
|
||||
assert(mapPatchSource.includes("patchTimings") && appSource.includes("result.patchTimings"), "patch generation returns and renders timing rows");
|
||||
assert(!mapPatchSource.includes("patchCandidateCacheKey") && !mapPatchSource.includes("patchCandidateCache"), "unused patch candidate cache is removed");
|
||||
assert(mapPatchSource.includes("getPatchAlphaCache") && mapPatchSource.includes("getPatchSourceIndexCache"), "patch generation caches alpha and source-index grids for merge work");
|
||||
assert(mapPatchSource.includes("attemptsRemaining = options.maxAttempts") && mapPatchSource.includes("connectorAttempts"), "patch connector pathfinding uses bounded attempts");
|
||||
assert(mapPatchSource.includes("minorRoads: 42") && mapPatchSource.includes("nationalRoads: 94")
|
||||
&& mapPatchSource.includes("expressways: 164") && mapPatchSource.includes("railways: 188")
|
||||
&& mapPatchSource.includes("PATCH_URBAN_RECALC_REACH = 112")
|
||||
&& mapPatchSource.includes("regionalRecalculationProbability"),
|
||||
"regional recomputation uses narrow local-road, wider national-road, and widest expressway/rail collars with distance-decaying selection probability");
|
||||
assert(worldMapSource.includes("shiftSelectionShape") && worldMapSource.includes("selectionShape = shiftSelectionShape"), "world expansion shifts stored lasso patch polygons");
|
||||
assert(municipalSource.includes("reconcileMunicipalMetadata") && mapOutputSource.includes("reconcileMunicipalMetadata") && mapPatchSource.includes("reconcileMunicipalMetadata"), "municipal metadata is reconciled in output and patch repair");
|
||||
assert(appSource.includes("mappedPref === id") && !appSource.includes("return nearestNamedAdminCenter(map, cellIndex, maxDistance, null)"), "tooltip municipal fallback requires exact coherent ids");
|
||||
|
|
@ -1090,7 +1128,7 @@ try {
|
|||
assert(map.naturalCompartmentId?.length === size && Array.isArray(map.naturalCompartments), "shared natural compartments are exposed");
|
||||
assert(map.adminDebug?.naturalCompartmentCount > 0 && map.adminDebug?.finalMunicipalityCount > 0, "natural compartments are generated before municipalities");
|
||||
assert(map.regionalDebug?.prefecturesGeneratedAfterMunicipalities === true && map.regionalDebug?.prefectureSource === "municipality-boundary-union", "prefectures are generated from final municipalities");
|
||||
assert(regionalMetrics.regionCount >= 4 && regionalMetrics.maxLandmassComponents === 1, "each non-sea prefecture region is connected after repair");
|
||||
assert(regionalMetrics.regionCount >= 2 && regionalMetrics.maxLandmassComponents === 1, "larger non-sea prefecture regions remain connected after repair");
|
||||
assert(regionalEnclaveCount(map) === 0, "final prefecture regions contain no one-region enclosed enclaves");
|
||||
const regionalBorders = regionalBorderMetrics(map);
|
||||
const hierarchyViolations = borderHierarchyViolations(map);
|
||||
|
|
@ -1106,7 +1144,8 @@ try {
|
|||
assert(regionalMetrics.tinyCount <= Math.max(1, Math.floor(regionalMetrics.regionCount * 0.12)) && regionalMetrics.medianArea >= 1200 && regionalMetrics.minArea >= 520, "regional prefectures avoid excessive tiny slivers");
|
||||
assert(Array.isArray(map.prefectureRegions) && map.prefectureRegions.length === regionalMetrics.regionCount, "prefecture region metadata exists for every region");
|
||||
assert(map.prefectureRegions.every((region) => region.name && Number.isFinite(region.x) && Number.isFinite(region.y) && region.area > 0 && map.prefectureRegionId[indexOf(region.x, region.y)] === region.id), "every prefecture region has a name and valid label point");
|
||||
assert(map.regionalDebug.finalRegionalMaxAreaShare < 0.38, "no single prefecture dominates regional land area");
|
||||
assert(map.regionalDebug.finalRegionalMaxAreaShare < 0.85, "larger prefectures retain more than one meaningful regional jurisdiction");
|
||||
assert(map.regionalDebug.finalRegionalMinMunicipalityCount >= 10, "each generated prefecture contains at least ten municipalities");
|
||||
assert(longStraightLowBarrierSegments(map, map.regionalPrefectureBorders, 22) === 0, "prefecture borders avoid long straight low-barrier cuts");
|
||||
assert(longStraightLowBarrierSegments(map, map.adminBorders, 20) === 0, "municipality borders avoid long straight low-barrier cuts");
|
||||
assert(Array.isArray(map.tributaryRivers) && Array.isArray(map.smallStreams), "river hierarchy arrays exist");
|
||||
|
|
@ -1234,7 +1273,7 @@ try {
|
|||
const adminNameDuplicateRatio = map.adminCenters.length ? 1 - new Set(map.adminCenters.map((item) => item.name)).size / map.adminCenters.length : 0;
|
||||
assert(adminNameDuplicateRatio < 0.22, "duplicate municipal names stay low");
|
||||
assert(map.adminDebug.naturalCompartmentCount > adminMetrics.municipalityCount, "natural compartments are finer than municipalities");
|
||||
assert(map.adminDebug.targetMunicipalityCount >= 20 && map.adminDebug.targetMunicipalityCount <= 50 && map.adminDebug.actualMunicipalityCount >= 18, "municipality target and actual counts are dense enough");
|
||||
assert(map.adminDebug.targetMunicipalityCount >= 20 && map.adminDebug.targetMunicipalityCount <= 62 && map.adminDebug.actualMunicipalityCount >= 18, "municipality target and actual counts are dense enough");
|
||||
assert(map.adminDebug.changedAfterCompartmentAssignment > 0, "municipal compartment assignment is active");
|
||||
assert(map.adminDebug.candidateSeedCount >= map.adminDebug.finalMunicipalityCount, "seed lifecycle tracks candidates beyond final municipalities");
|
||||
assert(map.adminDebug.absorbedSeedCount >= 0 && map.adminDebug.candidateSeedCount >= map.adminDebug.municipalOfficePointCount, "candidate municipality seeds resolve to offices or absorption");
|
||||
|
|
@ -1259,30 +1298,23 @@ try {
|
|||
assert(activePoolChars.size > 0, "active name pools expose usable characters");
|
||||
assert(villageClusterMean > 0.16, "villages prefer clustered valley, basin, coastal, and agricultural cells");
|
||||
assert(saneEndpointRatio >= 0.76, "transport endpoints stay near meaningful generated nodes");
|
||||
assert(
|
||||
map.adminCenters.length !== other.adminCenters.length ||
|
||||
map.villages.length !== other.villages.length ||
|
||||
map.markets.length !== other.markets.length,
|
||||
"feature counts vary between seeds"
|
||||
);
|
||||
|
||||
const againA = generateTestMap(999);
|
||||
const againB = generateTestMap(999);
|
||||
assert(JSON.stringify(againA.modernCities) === JSON.stringify(againB.modernCities), "generation is deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.entitiesForNames.map((item) => [item.id, item.name])) === JSON.stringify(againB.entitiesForNames.map((item) => [item.id, item.name])), "generated names are deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.adminCenters.map((item) => [item.id, item.x, item.y, item.name])) === JSON.stringify(againB.adminCenters.map((item) => [item.id, item.x, item.y, item.name])), "municipal centers are deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.adminId]) === JSON.stringify([...againB.adminId]), "municipal adminId snapping is deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.naturalCompartmentId]) === JSON.stringify([...againB.naturalCompartmentId]), "natural compartments are deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.prefectureRegionId]) === JSON.stringify([...againB.prefectureRegionId]), "regional prefecture ids are deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.municipalityToPrefectureId]) === JSON.stringify([...againB.municipalityToPrefectureId]), "municipality-to-prefecture ids are deterministic for the same seed");
|
||||
assert(arraysEqual(againA.adminId, againB.adminId), "municipal adminId snapping is deterministic for the same seed");
|
||||
assert(arraysEqual(againA.naturalCompartmentId, againB.naturalCompartmentId), "natural compartments are deterministic for the same seed");
|
||||
assert(arraysEqual(againA.prefectureRegionId, againB.prefectureRegionId), "regional prefecture ids are deterministic for the same seed");
|
||||
assert(arraysEqual(againA.municipalityToPrefectureId, againB.municipalityToPrefectureId), "municipality-to-prefecture ids are deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.regionalPrefectureBorders) === JSON.stringify(againB.regionalPrefectureBorders), "regional prefecture borders are deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.adminDebug) === JSON.stringify(againB.adminDebug), "admin debug metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify(againA.regionalDebug) === JSON.stringify(againB.regionalDebug), "regional debug metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify(deterministicTransportDebug(againA.transportDebug)) === JSON.stringify(deterministicTransportDebug(againB.transportDebug)), "transport debug metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify(transportConnectivityMetrics(againA)) === JSON.stringify(transportConnectivityMetrics(againB)), "transport connectivity metrics are deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.elevation]) === JSON.stringify([...againB.elevation]), "elevation is deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.ridgeField]) === JSON.stringify([...againB.ridgeField]), "ridge field is deterministic for the same seed");
|
||||
assert(JSON.stringify([...againA.river]) === JSON.stringify([...againB.river]), "river field is deterministic for the same seed");
|
||||
assert(arraysEqual(againA.elevation, againB.elevation), "elevation is deterministic for the same seed");
|
||||
assert(arraysEqual(againA.ridgeField, againB.ridgeField), "ridge field is deterministic for the same seed");
|
||||
assert(arraysEqual(againA.river, againB.river), "river field is deterministic for the same seed");
|
||||
assert(JSON.stringify(terrainCoreMetrics(againA)) === JSON.stringify(terrainCoreMetrics(againB)), "terrain debug metrics are deterministic for the same seed");
|
||||
|
||||
}
|
||||
|
|
@ -1306,7 +1338,12 @@ try {
|
|||
const seeded = generateTestMap(seedValue);
|
||||
if (seeded.prefecturalCapital?.name) capitalNames.push(seeded.prefecturalCapital.name);
|
||||
const metrics = terrainCoreMetrics(seeded);
|
||||
terrainSeedSummaries.push({ deposition: seeded.terrainTemplate.deposition, lowlandRatio: metrics.lowlandRatio });
|
||||
terrainSeedSummaries.push({
|
||||
seed: seedValue,
|
||||
deposition: seeded.terrainTemplate.deposition,
|
||||
lowlandRatio: metrics.lowlandRatio,
|
||||
featureCounts: [seeded.adminCenters.length, seeded.villages.length, seeded.markets.length],
|
||||
});
|
||||
assert(seeded.mainRivers.length > 0 && seeded.tributaryRivers.length > 0 && seeded.smallStreams.length > 0, `seed ${seedValue}: river hierarchy exists`);
|
||||
assert(metrics.mountainRatio > 0.08 && metrics.lowlandRatio > 0.06, `seed ${seedValue}: mountain and lowland terrain both exist`);
|
||||
assert(metrics.ridgeVariance > 0.003 && metrics.ridgeSinuosity > 0.010, `seed ${seedValue}: ridges have varied jagged structure`);
|
||||
|
|
@ -1344,16 +1381,20 @@ try {
|
|||
assert(seededAdminMetrics.denseUrbanRate < 0.52, `seed ${seedValue}: dense urban boundary crossing rate remains low`);
|
||||
assert(seededAdminMetrics.voronoiLikeRate < 0.66, `seed ${seedValue}: Voronoi-like municipal borders do not dominate`);
|
||||
}
|
||||
const featureCountA = terrainSeedSummaries.find((summary) => summary.seed === 12345)?.featureCounts;
|
||||
const featureCountB = terrainSeedSummaries.find((summary) => summary.seed === 54321)?.featureCounts;
|
||||
assert(featureCountA?.some((value, index) => value !== featureCountB?.[index]), "feature counts vary between seeds");
|
||||
assert(new Set(capitalNames).size > 1, "prefectural capital names vary across seeds");
|
||||
assert(capitalNames.some((name) => name !== blockedCapitalName), "prefectural capital is not always the repeated custom name");
|
||||
}
|
||||
|
||||
if (TEST_SUITE === "determinism" || TEST_SUITE.startsWith("determinism-")) {
|
||||
const suffixSeed = Number(TEST_SUITE.slice("determinism-".length));
|
||||
const suffix = TEST_SUITE.startsWith("determinism-") ? TEST_SUITE.slice("determinism-".length) : "";
|
||||
const suffixSeed = suffix ? Number(suffix) : NaN;
|
||||
const seedValue = Number.isFinite(suffixSeed) ? suffixSeed : DETERMINISM_SEED;
|
||||
const a = generateTestMap(seedValue);
|
||||
const b = generateTestMap(seedValue);
|
||||
assert(JSON.stringify([...a.adminId]) === JSON.stringify([...b.adminId]), `seed ${seedValue}: adminId is deterministic`);
|
||||
assert(arraysEqual(a.adminId, b.adminId), `seed ${seedValue}: adminId is deterministic`);
|
||||
assert(JSON.stringify(a.adminDebug) === JSON.stringify(b.adminDebug), `seed ${seedValue}: admin debug metrics are deterministic`);
|
||||
}
|
||||
|
||||
|
|
@ -1384,12 +1425,13 @@ try {
|
|||
assert(invalidLandCells === 0, `seed ${seed}: every prefecture land cell has a valid adminId`);
|
||||
assert(invalidRegionCells === 0, `seed ${seed}: every regional land cell has a valid regionId`);
|
||||
assert(seeded.adminBorders.length > 0, `seed ${seed}: municipal borders exist`);
|
||||
assert(seeded.regionalPrefectureBorders.length > 0, `seed ${seed}: regional prefecture borders exist`);
|
||||
const seededRegionalBorderMetrics = regionalBorderMetrics(seeded);
|
||||
assert(seededRegionalBorderMetrics.expected === 0 || seeded.regionalPrefectureBorders.length > 0, `seed ${seed}: regional prefecture borders exist whenever land-adjacent prefectures exist`);
|
||||
assert(seeded.regionalDebug?.prefecturesGeneratedAfterMunicipalities === true, `seed ${seed}: prefectures are generated after municipalities`);
|
||||
assert(seeded.regionalDebug?.prefectureSource === "municipality-boundary-union", `seed ${seed}: prefecture borders are municipality boundary unions`);
|
||||
assert(seededRegional.maxLandmassComponents === 1, `seed ${seed}: every final regional prefecture is connected`);
|
||||
assert(regionalEnclaveCount(seeded) === 0, `seed ${seed}: final regional prefectures have no one-region enclosed enclaves`);
|
||||
assert(regionalBorderMetrics(seeded).invalidSame === 0, `seed ${seed}: regional borders separate final prefecture ids`);
|
||||
assert(seededRegionalBorderMetrics.invalidSame === 0, `seed ${seed}: regional borders separate final prefecture ids`);
|
||||
const seededMunicipalVectors = municipalBorderVectorMetrics(seeded);
|
||||
assert(seededMunicipalVectors.invalid === 0 && seededMunicipalVectors.actual === seededMunicipalVectors.expected, `seed ${seed}: municipal vectors respect final prefecture hierarchy`);
|
||||
const seededHierarchy = borderHierarchyViolations(seeded);
|
||||
|
|
@ -1485,6 +1527,48 @@ try {
|
|||
} else {
|
||||
assert(true, "executed patch produced no region-tagged point requiring a regionId check");
|
||||
}
|
||||
|
||||
const expansionRect = {
|
||||
x0: world.originX + MAP_W - 48,
|
||||
y0: world.originY + 60,
|
||||
x1: world.originX + MAP_W + 52,
|
||||
y1: world.originY + 130,
|
||||
};
|
||||
const expansionBeforeGenerated = new Uint8Array(world.generatedMask);
|
||||
const expansionBeforeElevation = new Float32Array(world.fields.elevation);
|
||||
const expansionBeforeLanduse = new Uint8Array(world.fields.landuse);
|
||||
let existingOverlapCells = 0;
|
||||
const expansion = generatePatch(world, expansionRect, {
|
||||
patchMode: "expansion",
|
||||
terrainType: "auto",
|
||||
seed: 0x1234abcd,
|
||||
variant: 2,
|
||||
maxQualityRetries: 0,
|
||||
qualityTerrainAttempts: 1,
|
||||
includeSeamVisualization: true,
|
||||
acceptBestAvailableQuality: true,
|
||||
});
|
||||
let changedOverlapElevation = 0;
|
||||
let changedOverlapLanduse = 0;
|
||||
for (let y = expansionRect.y0; y < expansionRect.y1; y++) {
|
||||
for (let x = expansionRect.x0; x < expansionRect.x1; x++) {
|
||||
const i = y * world.width + x;
|
||||
if (!expansionBeforeGenerated[i]) continue;
|
||||
existingOverlapCells++;
|
||||
if (Math.abs(world.fields.elevation[i] - expansionBeforeElevation[i]) > 1e-6) changedOverlapElevation++;
|
||||
if (world.fields.landuse[i] !== expansionBeforeLanduse[i]) changedOverlapLanduse++;
|
||||
}
|
||||
}
|
||||
assert(expansion?.ok === true && existingOverlapCells > 0
|
||||
&& changedOverlapElevation > Math.max(24, existingOverlapCells * 0.15)
|
||||
&& changedOverlapLanduse > 0,
|
||||
`Expansion rewrites selected already-generated overlap instead of freezing it (elevation=${changedOverlapElevation}/${existingOverlapCells}, landuse=${changedOverlapLanduse})`);
|
||||
const regionalTransport = expansion?.humanGeography?.regionalTransportDebug;
|
||||
const regionalUrban = expansion?.humanGeography?.regionalUrbanRecalculation;
|
||||
assert((regionalTransport?.consideredPaths || 0) > 0 && (regionalTransport?.reroutedPaths || 0) > 0,
|
||||
`Expansion performs deterministic regional transport rerouting beyond the selected area (considered=${regionalTransport?.consideredPaths || 0}, rerouted=${regionalTransport?.reroutedPaths || 0})`);
|
||||
assert((regionalUrban?.modifiedCells || 0) > 0 && (regionalUrban?.maxOutsideDistanceModified || 0) > 0,
|
||||
`Expansion recalculates urban fields outside the selected area with distance-decaying probability (modified=${regionalUrban?.modifiedCells || 0}, outside=${Math.round(regionalUrban?.maxOutsideDistanceModified || 0)})`);
|
||||
}
|
||||
|
||||
if (suiteEnabled("patch-large")) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue