This commit is contained in:
33333-33333 2026-08-10 13:59:33 +09:00
commit 810ad6f5cb
33 changed files with 10710 additions and 1087 deletions

View file

@ -8,6 +8,7 @@ A browser-based procedural prefecture map generator.
- `styles/` - application styles
- `tests/` - reusable browser and Node.js tests
- `scripts/` - local development server helpers
- `docs/` - design history and release verification
- `archive/` - recoverable historical and legacy files, excluded from the active app
- `index.html` - application entry point
@ -29,10 +30,55 @@ Then open `http://127.0.0.1:8000/`.
## Tests
Run the complete Node.js test suite from the project root:
Run the aggregate Node.js regression runner:
```sh
node tests/test-all.mjs
```
The browser test page is available at `http://127.0.0.1:8000/tests/test.html`.
Heavy full-map shards can also be run independently, which is the recommended CI layout for memory-constrained workers:
```sh
node tests/additional-generation-unit.mjs
node tests/additional-generation-coverage-worker.mjs
node tests/test.js --suite=core
node tests/test.js --suite=terrain
node tests/test.js --suite=terrain-name
node tests/test.js --suite=admin
node tests/test.js --suite=patch
node tests/test.js --suite=patch-large
node tests/test.js --suite=determinism-114514
```
Run the focused additional-generation release gates:
```sh
node tests/patch-worker-mirror-sync.mjs
node tests/patch-worker-cancel.mjs
node tests/additional-generation-max-worker.mjs
```
The maximum-visible Expansion gate accepts a world seed and Variant through environment variables. CI should run these as independent matrix jobs rather than retaining multiple full worlds in one process:
```sh
PATCH_TEST_WORLD_SEED=12345 PATCH_TEST_VARIANT=0 node tests/additional-generation-max-worker.mjs
PATCH_TEST_WORLD_SEED=54321 PATCH_TEST_VARIANT=1 node tests/additional-generation-max-worker.mjs
```
Run the browser smoke profile:
```sh
node tests/run-additional-generation-browser.mjs
```
Run the 20-sample maximum-visible Expansion browser profile:
```sh
BROWSER_E2E_PROFILE=release \
BROWSER_E2E_WORKLOADS=expansion-max-visible \
node tests/run-additional-generation-browser.mjs
```
The browser test page is also available at `http://127.0.0.1:8000/tests/test.html`.
For the current r3 additional-generation verification record, see `docs/additional-generation-release-verification-20260810.md`.

View file

@ -8,4 +8,11 @@ function adapter(url){const code=`import {parentPort} from 'node:worker_threads'
const initial=generateMap(24681357,{terrainType:'auto',onProgress(){}}); const world=createWorldMap(initial); const edge=world.originX+MAP_W-1,cy=world.originY+Math.floor(MAP_H/2); const rect={x0:edge-40,y0:cy-100,x1:edge-40+280,y1:cy+100};
const worker=adapter(new URL('./mapPatchWorker.js',import.meta.url)); const preview=structuredClone(world); const transfer=Array.from(collectTransferableBuffers(preview)); const t=Date.now(); let last=0;
const message=await new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('timeout')),80000);worker.on('message',m=>{if(m?.type==='progress'){if(Date.now()-last>4000){last=Date.now(); console.error(Date.now()-t,m.progress?.label||m.progress?.key);}return;}clearTimeout(timer);resolve(m)});worker.on('error',e=>{clearTimeout(timer);reject(e)});worker.postMessage({id:1,world:preview,rect,options:{patchMode:'expansion',terrainType:'auto',seed:0x4a35b921,variant:1,maxQualityRetries:0,qualityTerrainAttempts:1,acceptBestAvailableQuality:true}},transfer);});
console.log(JSON.stringify({ms:Date.now()-t,outer:message.ok,inner:message.result?.ok,tiled:message.result?.tiledExpansion,tileCount:message.result?.tileCount,seam:message.result?.seamDiagnostics?.status,reason:message.result?.reason||message.error||null},null,2)); await worker.terminate(); process.exit(0);
const summary={ms:Date.now()-t,outer:message.ok,inner:message.result?.ok,tiled:message.result?.tiledExpansion,tileCount:message.result?.tileCount,seam:message.result?.seamDiagnostics?.status,reason:message.result?.reason||message.error||null};
console.log(JSON.stringify(summary,null,2));
assert.equal(summary.outer,true,'worker transport must succeed');
assert.equal(summary.inner,true,`large patch must succeed: ${summary.reason||'unknown failure'}`);
assert.equal(summary.tiled,true,'large selection must use tiled production generation');
assert.ok(summary.tileCount>=2,'large selection must execute multiple production tiles');
assert.ok(summary.ms<60000,`large worker patch exceeded 60 s budget: ${summary.ms} ms`);
await worker.terminate();

View file

@ -1,3 +1,4 @@
import assert from 'node:assert/strict';
import { generateMap } from './mapGenerator.js';
import { createWorldMap } from './worldMap.js';
import { generatePatch } from './mapPatch.js';
@ -8,10 +9,15 @@ const wa=createWorldMap(structuredClone(init)), wb=createWorldMap(structuredClon
const a=rectFor(wa,258,183), b=rectFor(wb,259,184);
const opts={patchMode:'expansion',terrainType:'auto',seed:0x4a35b921,variant:1,maxQualityRetries:0,qualityTerrainAttempts:1,acceptBestAvailableQuality:true};
const ra=generatePatch(wa,a,opts); const rb=generatePatch(wb,b,opts);
if(!ra.ok||!rb.ok){console.log(JSON.stringify({ra:{ok:ra.ok,code:ra.code,reason:ra.reason},rb:{ok:rb.ok,code:rb.code,reason:rb.reason}},null,2));process.exit(2)}
assert.equal(ra.ok,true,`258x183 patch failed: ${ra.reason||ra.code||'unknown'}`);
assert.equal(rb.ok,true,`259x184 patch failed: ${rb.reason||rb.code||'unknown'}`);
const fields=['elevation','sea','plain','agriculture','populationDensity','prefectureRegionId','adminId'];
const out={a:{tiled:!!ra.tiledExpansion,tileCount:ra.tileCount||1},b:{tiled:!!rb.tiledExpansion,tileCount:rb.tileCount||1},common:{}};
for(const name of fields){const A=wa.fields[name],B=wb.fields[name];let n=0,diff=0,sum=0,max=0;for(let y=a.y0;y<a.y1;y++)for(let x=a.x0;x<a.x1;x++){const i=y*wa.width+x;const av=A[i],bv=B[i];const d=Math.abs(Number(av)-Number(bv));n++;if(d>1e-9)diff++;sum+=d;max=Math.max(max,d)}out.common[name]={n,diff,rate:diff/n,meanAbs:sum/n,maxAbs:max};}
function boundarySet(world,field,rect){const f=world.fields[field],s=new Set();for(let y=rect.y0;y<rect.y1-1;y++)for(let x=rect.x0;x<rect.x1-1;x++){const i=y*world.width+x;if(f[i]!==f[i+1]||f[i]!==f[i+world.width])s.add(`${x},${y}`)}return s}
for(const field of ['prefectureRegionId','adminId']){const A=boundarySet(wa,field,a),B=boundarySet(wb,field,a);let xor=0;for(const k of A)if(!B.has(k))xor++;for(const k of B)if(!A.has(k))xor++;out.common[field+'Boundary']={a:A.size,b:B.size,xor,normalized:xor/Math.max(1,A.size+B.size)};}
console.log(JSON.stringify(out,null,2));
assert.ok(out.common.elevation.rate<=0.08,`common elevation changed too much: ${out.common.elevation.rate}`);
assert.ok(out.common.sea.rate<=0.05,`common sea mask changed too much: ${out.common.sea.rate}`);
assert.ok(out.common.prefectureRegionIdBoundary.normalized<=0.15,`prefecture boundary threshold instability: ${out.common.prefectureRegionIdBoundary.normalized}`);
assert.ok(out.common.adminIdBoundary.normalized<=0.15,`municipal boundary threshold instability: ${out.common.adminIdBoundary.normalized}`);

View file

@ -365,7 +365,7 @@ function naturalSeedScore(i, elevation, slope, river, ridgeField, valleyField, b
return stableInterior * 0.56 + lowland * 0.42 + mountainInterior * 0.32 + settlement * 0.26 + streamCorridor + hashSeededTie(...xyOf(i), seed) * 0.13 - slope[i] * 0.10;
}
function chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, seed) {
function chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, seed, progress = null) {
const { elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore } = fields;
const totalArea = landComponents.reduce((sum, cells) => sum + cells.length, 0);
const seeds = [];
@ -404,21 +404,50 @@ function chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, seed
const candidates = cells
.map((i) => ({ i, score: naturalSeedScore(i, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, plain, agriculture, populationDensity, landuse, naturalBarrierScore, seed + componentIndex * 1009) }))
.sort((a, b) => b.score - a.score);
progress?.(`natural seed component ${componentOrder + 1}/${sortedComponents.length} scored`);
const localSeeds = [];
const idealSpacing = Math.sqrt(area / Math.max(1, localTarget));
const spacingPasses = [0.95, 0.78, 0.62, 0.48, 0.34];
for (const factor of spacingPasses) {
for (let spacingPass = 0; spacingPass < spacingPasses.length; spacingPass++) {
const factor = spacingPasses[spacingPass];
const minDist = Math.max(2.2, idealSpacing * factor);
// The old loop compared every candidate with every accepted seed. A
// bucket with side=minDist is an exact filter: any seed that can satisfy
// the unchanged Math.hypot(... ) < minDist predicate must be in the same
// or one of the eight adjacent buckets.
const seedBuckets = new Map();
const bucketKey = (x, y) => `${Math.floor(x / minDist)},${Math.floor(y / minDist)}`;
const addSeedToBucket = (cellIndex) => {
const x = cellIndex % MAP_W;
const y = Math.floor(cellIndex / MAP_W);
const key = bucketKey(x, y);
let bucket = seedBuckets.get(key);
if (!bucket) seedBuckets.set(key, (bucket = []));
bucket.push(cellIndex);
};
for (const existing of localSeeds) addSeedToBucket(existing);
for (const candidate of candidates) {
if (localSeeds.length >= localTarget) break;
const [x, y] = xyOf(candidate.i);
const x = candidate.i % MAP_W;
const y = Math.floor(candidate.i / MAP_W);
const bx = Math.floor(x / minDist);
const by = Math.floor(y / minDist);
let ok = true;
for (const existing of localSeeds) {
const [ex, ey] = xyOf(existing);
if (Math.hypot(x - ex, y - ey) < minDist) { ok = false; break; }
for (let oy = -1; oy <= 1 && ok; oy++) {
for (let ox = -1; ox <= 1 && ok; ox++) {
for (const existing of seedBuckets.get(`${bx + ox},${by + oy}`) || []) {
const ex = existing % MAP_W;
const ey = Math.floor(existing / MAP_W);
if (Math.hypot(x - ex, y - ey) < minDist) { ok = false; break; }
}
}
}
if (ok) {
localSeeds.push(candidate.i);
addSeedToBucket(candidate.i);
}
if (ok) localSeeds.push(candidate.i);
}
progress?.(`natural seed component ${componentOrder + 1}/${sortedComponents.length}, spacing ${spacingPass + 1}/${spacingPasses.length}`);
if (localSeeds.length >= localTarget) break;
}
for (const i of localSeeds) {
@ -697,16 +726,18 @@ function splitNaturalCompartmentByAxis(unit, newId, compartmentId, fields, seed)
function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore = null, plain = null, agriculture = null, populationDensity = null, landuse = null, options = {}) {
const progress = typeof options.progress === "function" ? options.progress : null;
const naturalBarrierScore = buildNaturalBarrierScore(prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, crestCrossingScore, plain, agriculture, populationDensity, landuse);
progress?.("natural barrier field complete");
const cellClass = new Int16Array(SIZE);
cellClass.fill(-1);
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) cellClass[i] = classifyLandscapeCell(i, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse);
progress?.("natural landscape classes complete");
const watershedId = options.watershedId || null;
const fields = { elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, naturalBarrierScore, cellClass, watershedId };
const landComponents = collectNaturalGrowthComponents(prefectureMask, sea, watershedId);
const landArea = landComponents.reduce((sum, cells) => sum + cells.length, 0);
const requestedTarget = options.targetCompartmentCount || clamp(Math.round(landArea / 34), 40, 360);
const targetCount = clamp(Math.round(requestedTarget), Math.min(1, landArea), Math.max(1, Math.floor(landArea / 8)));
const { seeds } = chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, options.seed || 0);
const { seeds } = chooseNaturalCompartmentSeeds(landComponents, targetCount, fields, options.seed || 0, progress);
progress?.(`natural seeds chosen: ${seeds.length}/${targetCount}`);
const compartmentId = new Int32Array(SIZE);
compartmentId.fill(-1);
@ -741,7 +772,7 @@ function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, r
let compartments = buildUnitsFromAssignment(compartmentId, cellClass, prefectureMask, sea, fields);
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
mergeTinyLandscapeUnits(compartmentId, compartments, 9);
mergeWeakArtificialLandscapeUnits(compartmentId, compartments, fields, prefectureMask, sea, Math.max(42, Math.round(landArea / Math.max(1, targetCount) * 2.15)), 5);
mergeWeakArtificialLandscapeUnits(compartmentId, compartments, fields, prefectureMask, sea, Math.max(42, Math.round(landArea / Math.max(1, targetCount) * 2.15)), 5, progress);
progress?.(`natural post-split units: ${compartments.filter((unit) => unit && unit.area > 0).length}`);
splitDisconnectedCompartments(compartmentId, compartments, prefectureMask, sea);
splitCompartmentsByWatershed(compartmentId, compartments, fields);
@ -752,7 +783,9 @@ function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, r
const maxNaturalCompartmentArea = options.maxNaturalCompartmentArea || Math.max(28, Math.round(landArea / Math.max(1, targetCount) * 1.55));
let guard = Math.max(60, targetCount * 2);
let splitIterations = 0;
while (guard-- > 0) {
if (splitIterations++ % 4 === 0) progress?.(`natural compact split ${splitIterations}/${Math.max(60, targetCount * 2)}`);
let active = compartments.filter((unit) => unit && unit.area > 0);
const needMore = active.length < targetCount;
const worst = active
@ -781,9 +814,12 @@ function buildSeededNaturalCompartments(prefectureMask, sea, elevation, slope, r
}
}
mergeWeakArtificialLandscapeUnits(compartmentId, compartments, fields, prefectureMask, sea, Math.max(48, Math.round(landArea / Math.max(1, targetCount) * 2.05)), 4);
progress?.("natural compact split complete");
mergeWeakArtificialLandscapeUnits(compartmentId, compartments, fields, prefectureMask, sea, Math.max(48, Math.round(landArea / Math.max(1, targetCount) * 2.05)), 4, progress);
progress?.("natural weak-boundary merge complete");
splitDisconnectedCompartments(compartmentId, compartments, prefectureMask, sea);
splitCompartmentsByWatershed(compartmentId, compartments, fields);
progress?.("natural connectivity and watershed split complete");
compartments = renumberCompartments(compartmentId, compartments, prefectureMask, sea);
refreshAllCompartmentStats(compartments, fields);
rebuildLandscapeUnitAdjacency(compartmentId, compartments, naturalBarrierScore, prefectureMask, sea);
@ -857,12 +893,13 @@ function mergeUnitInto(unitId, units, fromId, toId, fields) {
return true;
}
function mergeWeakArtificialLandscapeUnits(unitId, units, fields, prefectureMask, sea, maxMergedArea = 84, passes = 5) {
function mergeWeakArtificialLandscapeUnits(unitId, units, fields, prefectureMask, sea, maxMergedArea = 84, passes = 5, progress = null) {
// Seeded graph growth can create diagonal stair-step borders in uniform plains
// and gentle hills. If the shared edge is weak, balanced H/V, and the two
// sides are the same natural group, merge it instead of preserving an
// artificial Voronoi-like cut.
for (let pass = 0; pass < passes; pass++) {
progress?.(`natural weak-boundary merge pass ${pass + 1}/${passes}`);
rebuildLandscapeUnitAdjacency(unitId, units, fields.naturalBarrierScore, prefectureMask, sea);
let best = null;
let bestScore = 0.0;
@ -1235,4 +1272,3 @@ function applyLandscapeUnitAdminPartition(adminId, prefectureMask, sea, elevatio
voronoiLikeRateAfter: weakVoronoiLikeRate(adminId, adminCenters, prefectureMask, sea, naturalBarrierScore),
};
}

1557
src/app.js

File diff suppressed because it is too large Load diff

430
src/committedWorldDelta.js Normal file
View file

@ -0,0 +1,430 @@
function typedArrayConstructor(name) {
return {
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
Int32Array, Uint32Array, Float32Array, Float64Array,
}[name] || null;
}
function applyTypedRowDelta(current, delta) {
if (!delta) return current;
const Constructor = typedArrayConstructor(delta.constructorName);
if (!Constructor) throw new Error(`Unsupported delta field type ${delta.constructorName}.`);
if (delta.replace) return new Constructor(delta.replace);
const target = ArrayBuffer.isView(current) && current.constructor?.name === delta.constructorName && current.length === delta.length
? current
: new Constructor(delta.length);
for (const row of delta.rows || []) target.set(row.values, row.start);
return target;
}
function applyExactObjectDelta(target = {}, delta = {}, { cloneValues = true } = {}) {
for (const key of delta.removed || []) delete target[key];
for (const [key, value] of Object.entries(delta.set || {})) target[key] = cloneValues ? structuredClone(value) : value;
for (const [key, splice] of Object.entries(delta.arraySplices || {})) {
const current = target[key];
if (!Array.isArray(current)) throw new Error(`Cannot apply array splice delta to non-array metadata key ${key}.`);
const start = Math.max(0, Math.min(current.length, Math.floor(Number(splice?.start || 0))));
const deleteCount = Math.max(0, Math.min(current.length - start, Math.floor(Number(splice?.deleteCount || 0))));
const items = Array.isArray(splice?.items) ? splice.items : [];
const inserted = cloneValues ? structuredClone(items) : items;
target[key] = current.slice(0, start).concat(inserted, current.slice(start + deleteCount));
}
return target;
}
export function applyCommittedWorldDelta(world, delta, { consumeMetadata = false, copyOnWrite = false } = {}) {
if (!world || !delta) throw new Error("Committed mirror delta is missing.");
if (world.width !== delta.width || world.height !== delta.height) {
throw new Error(`Committed mirror dimensions changed unexpectedly (${world.width}x${world.height} -> ${delta.width}x${delta.height}).`);
}
for (const [name, fieldDelta] of Object.entries(delta.fields || {})) {
if (fieldDelta.remove) delete world.fields[name];
else {
const current = world.fields?.[name];
const writable = copyOnWrite && ArrayBuffer.isView(current) && !fieldDelta.replace
? new current.constructor(current)
: current;
world.fields[name] = applyTypedRowDelta(writable, fieldDelta);
}
}
const generatedMask = copyOnWrite && delta.generatedMask && ArrayBuffer.isView(world.generatedMask) && !delta.generatedMask.replace
? new world.generatedMask.constructor(world.generatedMask)
: world.generatedMask;
world.generatedMask = applyTypedRowDelta(generatedMask, delta.generatedMask);
if (delta.sourceMapDelta || delta.metaDelta) {
// Clone all changed metadata in one graph. sourceMap diagnostics and
// lastPatchResult often share the same seam/path objects; cloning each key
// separately multiplied both allocation and retained heap.
// postMessage already gives the main thread an isolated object graph, and
// the Worker ACK consumes its retained delta exactly once. Those hot paths
// can adopt the delta values directly instead of cloning every changed
// feature/path/diagnostic a second time at peak memory. Keep clone-by-default
// for reusable library callers.
const exactValues = consumeMetadata ? {
sourceSet: delta.sourceMapDelta?.set || {},
metaSet: delta.metaDelta?.set || {},
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
metaArraySplices: delta.metaDelta?.arraySplices || {},
} : structuredClone({
sourceSet: delta.sourceMapDelta?.set || {},
metaSet: delta.metaDelta?.set || {},
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
metaArraySplices: delta.metaDelta?.arraySplices || {},
});
if (delta.sourceMapDelta) {
world.sourceMap = applyExactObjectDelta(world.sourceMap || {}, {
...delta.sourceMapDelta,
set: exactValues.sourceSet,
arraySplices: exactValues.sourceArraySplices,
}, { cloneValues: false });
}
if (delta.metaDelta) {
applyExactObjectDelta(world, {
...delta.metaDelta,
set: exactValues.metaSet,
arraySplices: exactValues.metaArraySplices,
}, { cloneValues: false });
}
}
else world.sourceMap = structuredClone(delta.sourceMap || {});
if (!delta.metaDelta) {
for (const key of delta.removedMetaKeys || []) delete world[key];
for (const [key, value] of Object.entries(delta.meta || {})) world[key] = structuredClone(value);
}
return world;
}
export function materializeCommittedWorldDelta(baseWorld, delta, { consumeMetadata = false } = {}) {
if (!baseWorld) throw new Error("Committed base world is missing.");
// Patch previews are immutable views. Copy the world containers, then clone
// only typed fields touched by row deltas. Exact metadata application replaces
// changed roots/arrays, so untouched production metadata can remain shared.
const previewWorld = {
...baseWorld,
fields: { ...(baseWorld.fields || {}) },
sourceMap: { ...(baseWorld.sourceMap || {}) },
};
return applyCommittedWorldDelta(previewWorld, delta, { consumeMetadata, copyOnWrite: true });
}
function cancellationError(message = "Committed world delta materialization cancelled.") {
if (typeof DOMException === "function") return new DOMException(message, "AbortError");
const error = new Error(message);
error.name = "AbortError";
return error;
}
async function cloneTypedArrayCooperatively(source, yieldControl, shouldCancel, chunkBytes) {
if (!ArrayBuffer.isView(source) || source instanceof DataView) return source;
const target = new source.constructor(source.length);
const bytesPerElement = Math.max(1, source.BYTES_PER_ELEMENT || 1);
const elementsPerChunk = Math.max(1, Math.floor(chunkBytes / bytesPerElement));
for (let offset = 0; offset < source.length; offset += elementsPerChunk) {
if (shouldCancel()) throw cancellationError();
const end = Math.min(source.length, offset + elementsPerChunk);
target.set(source.subarray(offset, end), offset);
if (end < source.length) await yieldControl();
}
return target;
}
async function applyTypedRowDeltaCooperatively(current, delta, options) {
if (!delta) return current;
const Constructor = typedArrayConstructor(delta.constructorName);
if (!Constructor) throw new Error(`Unsupported delta field type ${delta.constructorName}.`);
const { yieldControl, shouldCancel, chunkBytes } = options;
if (delta.replace) {
// The main thread owns transferred replacement buffers exclusively. Adopt
// them directly instead of making a second full-size copy at peak memory.
if (ArrayBuffer.isView(delta.replace) && delta.replace.constructor === Constructor) return delta.replace;
return cloneTypedArrayCooperatively(new Constructor(delta.replace), yieldControl, shouldCancel, chunkBytes);
}
let target;
if (ArrayBuffer.isView(current) && current.constructor?.name === delta.constructorName && current.length === delta.length) {
target = await cloneTypedArrayCooperatively(current, yieldControl, shouldCancel, chunkBytes);
} else {
target = new Constructor(delta.length);
}
let bytesSinceYield = 0;
for (const row of delta.rows || []) {
if (shouldCancel()) throw cancellationError();
target.set(row.values, row.start);
bytesSinceYield += row.values?.byteLength || 0;
if (bytesSinceYield >= chunkBytes) {
bytesSinceYield = 0;
await yieldControl();
}
}
return target;
}
export async function materializeCommittedWorldDeltaCooperative(baseWorld, delta, {
consumeMetadata = false,
yieldControl = () => Promise.resolve(),
shouldCancel = () => false,
chunkBytes = 4 * 1024 * 1024,
} = {}) {
if (!baseWorld) throw new Error("Committed base world is missing.");
if (!delta) throw new Error("Committed mirror delta is missing.");
if (baseWorld.width !== delta.width || baseWorld.height !== delta.height) {
throw new Error(`Committed mirror dimensions changed unexpectedly (${baseWorld.width}x${baseWorld.height} -> ${delta.width}x${delta.height}).`);
}
const previewWorld = {
...baseWorld,
fields: { ...(baseWorld.fields || {}) },
sourceMap: { ...(baseWorld.sourceMap || {}) },
};
for (const [name, fieldDelta] of Object.entries(delta.fields || {})) {
if (shouldCancel()) throw cancellationError();
if (fieldDelta.remove) delete previewWorld.fields[name];
else previewWorld.fields[name] = await applyTypedRowDeltaCooperatively(previewWorld.fields[name], fieldDelta, {
yieldControl, shouldCancel, chunkBytes,
});
await yieldControl();
}
if (delta.generatedMask) {
previewWorld.generatedMask = await applyTypedRowDeltaCooperatively(previewWorld.generatedMask, delta.generatedMask, {
yieldControl, shouldCancel, chunkBytes,
});
await yieldControl();
}
if (shouldCancel()) throw cancellationError();
if (delta.sourceMapDelta || delta.metaDelta) {
const exactValues = consumeMetadata ? {
sourceSet: delta.sourceMapDelta?.set || {},
metaSet: delta.metaDelta?.set || {},
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
metaArraySplices: delta.metaDelta?.arraySplices || {},
} : structuredClone({
sourceSet: delta.sourceMapDelta?.set || {},
metaSet: delta.metaDelta?.set || {},
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
metaArraySplices: delta.metaDelta?.arraySplices || {},
});
if (delta.sourceMapDelta) {
previewWorld.sourceMap = applyExactObjectDelta(previewWorld.sourceMap || {}, {
...delta.sourceMapDelta,
set: exactValues.sourceSet,
arraySplices: exactValues.sourceArraySplices,
}, { cloneValues: false });
await yieldControl();
}
if (shouldCancel()) throw cancellationError();
if (delta.metaDelta) {
applyExactObjectDelta(previewWorld, {
...delta.metaDelta,
set: exactValues.metaSet,
arraySplices: exactValues.metaArraySplices,
}, { cloneValues: false });
await yieldControl();
}
} else {
previewWorld.sourceMap = consumeMetadata ? (delta.sourceMap || {}) : structuredClone(delta.sourceMap || {});
}
if (!delta.metaDelta) {
for (const key of delta.removedMetaKeys || []) delete previewWorld[key];
for (const [key, value] of Object.entries(delta.meta || {})) {
if (shouldCancel()) throw cancellationError();
previewWorld[key] = consumeMetadata ? value : structuredClone(value);
await yieldControl();
}
}
return previewWorld;
}
function mixHashText(hash, text) {
let value = hash >>> 0;
for (let index = 0; index < text.length; index++) {
value ^= text.charCodeAt(index);
value = Math.imul(value, 16777619) >>> 0;
}
return value;
}
function mixHashValue(hash, value) {
let next = hash >>> 0;
if (value == null) return mixHashText(next, String(value));
if (ArrayBuffer.isView(value)) {
next = mixHashText(next, `${value.constructor.name}:${value.length}:`);
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
for (let index = 0; index < bytes.length; index++) {
next ^= bytes[index];
next = Math.imul(next, 16777619) >>> 0;
}
return next;
}
if (Array.isArray(value)) {
next = mixHashText(next, `[${value.length}:`);
for (const item of value) next = mixHashValue(next, item);
return next;
}
if (value instanceof Map) {
const entries = [...value.entries()].sort(([a], [b]) => String(a).localeCompare(String(b)));
return mixHashValue(mixHashText(next, `Map:${entries.length}:`), entries);
}
if (value instanceof Set) return mixHashValue(mixHashText(next, `Set:${value.size}:`), [...value].sort());
if (typeof value === "object") {
const keys = Object.keys(value).sort();
next = mixHashText(next, `{${keys.length}:`);
for (const key of keys) {
next = mixHashText(next, key);
next = mixHashValue(next, value[key]);
}
return next;
}
return mixHashText(next, `${typeof value}:${String(value)}`);
}
export function hashCommittedWorld(world) {
let hash = 2166136261 >>> 0;
hash = mixHashText(hash, `${world?.width || 0}x${world?.height || 0}|${world?.originX || 0},${world?.originY || 0}`);
for (const name of Object.keys(world?.fields || {}).sort()) {
hash = mixHashText(hash, `|${name}:`);
const field = world.fields[name];
if (!ArrayBuffer.isView(field)) continue;
const bytes = new Uint8Array(field.buffer, field.byteOffset, field.byteLength);
for (let index = 0; index < bytes.length; index++) {
hash ^= bytes[index];
hash = Math.imul(hash, 16777619) >>> 0;
}
}
if (ArrayBuffer.isView(world?.generatedMask)) {
hash = mixHashText(hash, "|generatedMask:");
const bytes = new Uint8Array(world.generatedMask.buffer, world.generatedMask.byteOffset, world.generatedMask.byteLength);
for (let index = 0; index < bytes.length; index++) {
hash ^= bytes[index];
hash = Math.imul(hash, 16777619) >>> 0;
}
}
hash = mixHashText(hash, "|sourceMap:");
hash = mixHashValue(hash, world?.sourceMap || {});
const meta = {};
for (const [key, value] of Object.entries(world || {})) {
if (key === "fields" || key === "generatedMask" || key === "sourceMap") continue;
meta[key] = value;
}
hash = mixHashText(hash, "|meta:");
hash = mixHashValue(hash, meta);
return hash.toString(16).padStart(8, "0");
}
async function cooperativeHashYield(state) {
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
state.pending = 0;
await state.yieldControl();
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
}
async function mixHashTextAsync(hash, text, state) {
let value = hash >>> 0;
let index = 0;
while (index < text.length) {
const start = index;
const capacity = Math.max(1, state.yieldEvery - state.pending);
const end = Math.min(text.length, index + capacity);
for (; index < end; index++) {
value ^= text.charCodeAt(index);
value = Math.imul(value, 16777619) >>> 0;
}
state.pending += end - start;
if (state.pending >= state.yieldEvery) await cooperativeHashYield(state);
}
return value;
}
async function mixHashBytesAsync(hash, bytes, state) {
let value = hash >>> 0;
let index = 0;
while (index < bytes.length) {
const start = index;
const capacity = Math.max(1, state.yieldEvery - state.pending);
const end = Math.min(bytes.length, index + capacity);
for (; index < end; index++) {
value ^= bytes[index];
value = Math.imul(value, 16777619) >>> 0;
}
state.pending += end - start;
if (state.pending >= state.yieldEvery) await cooperativeHashYield(state);
}
return value;
}
async function mixHashValueAsync(hash, value, state) {
let next = hash >>> 0;
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
if (value == null) return mixHashTextAsync(next, String(value), state);
if (ArrayBuffer.isView(value)) {
next = await mixHashTextAsync(next, `${value.constructor.name}:${value.length}:`, state);
return mixHashBytesAsync(next, new Uint8Array(value.buffer, value.byteOffset, value.byteLength), state);
}
if (Array.isArray(value)) {
next = await mixHashTextAsync(next, `[${value.length}:`, state);
for (const item of value) next = await mixHashValueAsync(next, item, state);
return next;
}
if (value instanceof Map) {
const entries = [...value.entries()].sort(([a], [b]) => String(a).localeCompare(String(b)));
next = await mixHashTextAsync(next, `Map:${entries.length}:`, state);
return mixHashValueAsync(next, entries, state);
}
if (value instanceof Set) {
next = await mixHashTextAsync(next, `Set:${value.size}:`, state);
return mixHashValueAsync(next, [...value].sort(), state);
}
if (typeof value === "object") {
const keys = Object.keys(value).sort();
next = await mixHashTextAsync(next, `{${keys.length}:`, state);
for (const key of keys) {
next = await mixHashTextAsync(next, key, state);
next = await mixHashValueAsync(next, value[key], state);
}
return next;
}
return mixHashTextAsync(next, `${typeof value}:${String(value)}`, state);
}
// Bit-identical cooperative counterpart to hashCommittedWorld(). Yielding is
// only inserted between chunks; byte/text order and FNV-1a arithmetic are
// unchanged. This lets the main thread verify a transferred transactional
// preview without creating an uncancellable multi-megabyte long task.
export async function hashCommittedWorldAsync(world, {
yieldEvery = 262_144,
yieldControl = null,
shouldAbort = () => false,
} = {}) {
const fallbackYield = () => {
if (typeof globalThis.scheduler?.yield === "function") return globalThis.scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
};
const state = {
yieldEvery: Math.max(1, Math.floor(Number(yieldEvery) || 262_144)),
pending: 0,
shouldAbort: typeof shouldAbort === "function" ? shouldAbort : () => false,
yieldControl: typeof yieldControl === "function" ? yieldControl : fallbackYield,
};
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
let hash = 2166136261 >>> 0;
hash = await mixHashTextAsync(hash, `${world?.width || 0}x${world?.height || 0}|${world?.originX || 0},${world?.originY || 0}`, state);
for (const name of Object.keys(world?.fields || {}).sort()) {
hash = await mixHashTextAsync(hash, `|${name}:`, state);
const field = world.fields[name];
if (!ArrayBuffer.isView(field)) continue;
hash = await mixHashBytesAsync(hash, new Uint8Array(field.buffer, field.byteOffset, field.byteLength), state);
}
if (ArrayBuffer.isView(world?.generatedMask)) {
hash = await mixHashTextAsync(hash, "|generatedMask:", state);
hash = await mixHashBytesAsync(hash, new Uint8Array(world.generatedMask.buffer, world.generatedMask.byteOffset, world.generatedMask.byteLength), state);
}
hash = await mixHashTextAsync(hash, "|sourceMap:", state);
hash = await mixHashValueAsync(hash, world?.sourceMap || {}, state);
const meta = {};
for (const [key, value] of Object.entries(world || {})) {
if (key === "fields" || key === "generatedMask" || key === "sourceMap") continue;
meta[key] = value;
}
hash = await mixHashTextAsync(hash, "|meta:", state);
hash = await mixHashValueAsync(hash, meta, state);
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
return hash.toString(16).padStart(8, "0");
}

View file

@ -7,7 +7,7 @@ import { buildFeatureLanduse } from "./mapFeatureLanduse.js";
import { buildSettlementDemandFields } from "./mapFeatureSettlements.js";
import { buildFeatureTransportCostFields } from "./mapFeatureTransportTools.js";
import { buildCoarseCostGraph, refineCoarsePath, routeCoarsePath } from "./mapTransportGraph.js";
import { labelOccupancyComponents, normalizeTransportPathSet, smoothRasterPath } from "./mapTransportUtils.js";
import { getRadialInfluenceKernel, labelOccupancyComponents, normalizeTransportPathSet, smoothRasterPath } from "./mapTransportUtils.js";
// Lightweight Human Geography V2
// --------------------------------
@ -28,6 +28,7 @@ export function generateMapFeatures(seed, terrain, options = {}) {
const largePatchTile = options?.largeExpansionTile === true;
const topCenterSuppression = clamp(Number.isFinite(options?.topCenterSuppression) ? options.topCenterSuppression : (patchMode ? 0.68 : 0), 0, 0.95);
const featureTimings = [];
const generationProgress = options?.onProgress;
let timingMark = nowMs();
function markFeatureTiming(key) {
const t = nowMs();
@ -688,6 +689,8 @@ if (isRegionalCapital) {
const componentCapitalInfluence = influenceFromPoints(modernCities.filter((p) => p.isPrefecturalCapital), 16, () => 5.0);
const corridorSkeleton = new Uint8Array(SIZE);
const corridorAllowanceField = new Float64Array(SIZE);
const corridorTerrainFlowField = new Float64Array(SIZE);
for (let i = 0; i < SIZE; i++) {
if (sea[i]) continue;
corridorSkeleton[i] = Math.round(clamp(
@ -701,11 +704,19 @@ if (isRegionalCapital) {
ridgeField[i] * 0.22 -
slope[i] * 0.28
) * 255);
corridorAllowanceField[i] = clamp(
settlementDemand[i] * 0.58 + valleyField[i] * 0.42 + coastalLowland[i] * 0.36
- plain[i] * 0.18 - agriculture[i] * 0.14
);
corridorTerrainFlowField[i] = clamp(
valleyField[i] * 0.54 + coastalLowland[i] * 0.28 + plain[i] * 0.16
+ (passSuitability?.[i] || 0) * 0.34 - ridgeField[i] * 0.24 - slope[i] * 0.22
);
}
function corridorAllowance(i) {
return clamp(settlementDemand[i] * 0.58 + valleyField[i] * 0.42 + coastalLowland[i] * 0.36 - plain[i] * 0.18 - agriculture[i] * 0.14);
return corridorAllowanceField[i] || 0;
}
function endpointSupport(i) {
@ -744,6 +755,27 @@ if (isRegionalCapital) {
closed: new Int32Array(SIZE),
epoch: 0,
};
let corridorSurfaceNoise = null;
let routeProgressSerial = 0;
function getCorridorSurfaceNoise() {
if (corridorSurfaceNoise) return corridorSurfaceNoise;
corridorSurfaceNoise = new Float64Array(SIZE);
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) corridorSurfaceNoise[indexOf(x, y)] = valueNoise(x, y, seed + 13941, 18);
if (y % 32 === 31 || y + 1 === MAP_H) {
generationProgress?.({
status: "route-heartbeat",
key: "route:surface-noise",
phase: "transport-routing",
workUnitId: "route-surface-noise",
label: `Preparing shared route surface field ${y + 1}/${MAP_H} rows`,
completed: y + 1,
total: MAP_H,
});
}
}
return corridorSurfaceNoise;
}
function traceCorridorByCost(start, goalRegionPredicate, costField, penaltyField, options = {}) {
if (!start || !inside(start.x, start.y)) return [];
@ -761,7 +793,7 @@ if (isRegionalCapital) {
seen[startIndex] = epoch;
score[startIndex] = 0;
cameFrom[startIndex] = -1;
heap.push({ i: startIndex, f: 0 });
heap.push({ i: startIndex, f: 0, g: 0 });
const curvePenalty = options.curvePenalty ?? 0.12;
const penaltyStrength = options.penaltyStrength ?? 1.0;
const sameRegion = options.regionId ?? regionIdAt(start.x, start.y);
@ -770,12 +802,34 @@ if (isRegionalCapital) {
const bounds = options.bounds || null;
const goalHint = options.goalHint || null;
const heuristicWeight = options.heuristicWeight ?? 0;
const surfaceNoise = (options.surfaceGrain ?? 0) !== 0 ? getCorridorSurfaceNoise() : null;
// A display label such as "national corridor" is intentionally reused for
// many independent A* searches. Progress invariants must therefore key on
// one finite search invocation, never on that human-readable label. The
// ordinal is deterministic within a candidate and has no effect on output.
const routeWorkUnitId = String(options.progressWorkUnitId || `route-search-${++routeProgressSerial}`);
let goalIndex = -1;
let expanded = 0;
while (heap.length && expanded++ < maxExpanded) {
if (expanded % 2048 === 0) {
generationProgress?.({
status: "route-heartbeat",
key: `route:${options.progressLabel || "corridor"}`,
phase: "transport-routing",
workUnitId: routeWorkUnitId,
label: `Routing ${options.progressLabel || "transport corridor"}: ${expanded}/${maxExpanded} nodes`,
completed: expanded,
total: maxExpanded,
});
}
const current = heap.pop();
if (!current || closed[current.i] === epoch) continue;
// A cell may be queued more than once when a cheaper route is found.
// Its older entry always has a larger f for the same heuristic, so
// dropping that stale entry preserves the selected route and avoids
// expanding thousands of superseded nodes.
if (current.g !== score[current.i]) continue;
closed[current.i] = epoch;
const cx = current.i % MAP_W;
const cy = Math.floor(current.i / MAP_W);
@ -783,6 +837,12 @@ if (isRegionalCapital) {
goalIndex = current.i;
break;
}
const prev = cameFrom[current.i];
const previousDx = prev >= 0 ? cx - (prev % MAP_W) : 0;
const previousDy = prev >= 0 ? cy - Math.floor(prev / MAP_W) : 0;
const currentScore = score[current.i];
const terrainFlowStrength = options.terrainFlowBias ?? 0;
const surfaceGrainStrength = options.surfaceGrain ?? 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
@ -793,33 +853,22 @@ if (isRegionalCapital) {
const ni = indexOf(nx, ny);
if (closed[ni] === epoch || sea[ni] || costField[ni] >= INF) continue;
if (sameRegion >= 0 && options.keepRegion !== false && regionIdAt(nx, ny) !== sameRegion) continue;
const prev = cameFrom[current.i];
let turn = 0;
if (prev >= 0) {
const px = prev % MAP_W;
const py = Math.floor(prev / MAP_W);
const ax = cx - px;
const ay = cy - py;
turn = Math.abs(ax * dy - ay * dx) > 0 ? curvePenalty : 0;
turn = Math.abs(previousDx * dy - previousDy * dx) > 0 ? curvePenalty : 0;
}
const existing = penaltyField?.[ni] || 0;
const antiConcentration = existing * penaltyStrength * (1 - corridorAllowance(ni) * 0.72);
const terrainFlowBias = (options.terrainFlowBias ?? 0) * clamp(
valleyField[ni] * 0.54 +
coastalLowland[ni] * 0.28 +
plain[ni] * 0.16 +
(passSuitability?.[ni] || 0) * 0.34 -
ridgeField[ni] * 0.24 -
slope[ni] * 0.22
);
const surfaceGrain = (options.surfaceGrain ?? 0) * valueNoise(nx, ny, seed + 13941, 18);
const nd = score[current.i] + Math.max(0.08, costField[ni] + antiConcentration + turn - terrainFlowBias + surfaceGrain) * Math.hypot(dx, dy);
const terrainFlowBias = terrainFlowStrength * corridorTerrainFlowField[ni];
const surfaceGrain = surfaceGrainStrength * (surfaceNoise?.[ni] || 0);
const stepDistance = dx && dy ? Math.SQRT2 : 1;
const nd = currentScore + Math.max(0.08, costField[ni] + antiConcentration + turn - terrainFlowBias + surfaceGrain) * stepDistance;
if (seen[ni] !== epoch || nd < score[ni]) {
seen[ni] = epoch;
score[ni] = nd;
cameFrom[ni] = current.i;
const h = goalHint ? Math.hypot(nx - goalHint.x, ny - goalHint.y) * heuristicWeight : 0;
heap.push({ i: ni, f: nd + h });
heap.push({ i: ni, f: nd + h, g: score[ni] });
}
}
}
@ -834,6 +883,22 @@ if (isRegionalCapital) {
}
function addCorridorInfluencePenalty(penaltyField, corridor, radius = 7, strength = 0.35) {
const kernel = radius > 0 ? getRadialInfluenceKernel(radius, 1) : null;
if (kernel) {
for (const [x, y] of corridor || []) {
for (let k = 0; k < kernel.length; k++) {
const nx = x + kernel.dx[k];
const ny = y + kernel.dy[k];
if (nx < 0 || ny < 0 || nx >= MAP_W || ny >= MAP_H) continue;
const i = ny * MAP_W + nx;
if (sea[i]) continue;
const openPlain = clamp(plain[i] * 0.54 + agriculture[i] * 0.34 - settlementDemand[i] * 0.24 - valleyField[i] * 0.22 - coastalLowland[i] * 0.18);
const allowParallel = corridorAllowance(i);
penaltyField[i] = Math.max(penaltyField[i], strength * kernel.weight[k] * (0.48 + openPlain * 1.15 - allowParallel * 0.42));
}
}
return;
}
for (const [x, y] of corridor || []) {
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
@ -1606,6 +1671,7 @@ if (isRegionalCapital) {
bounds,
goalHint: options.goalHint || target,
heuristicWeight: options.heuristicWeight ?? (mode === "expressway" ? 0.66 : mode === "rail" ? 0.54 : mode === "national" ? 0.46 : 0.30),
progressLabel: `${mode} corridor`,
}
);
}
@ -2466,6 +2532,152 @@ const premodernRoads = [];
transportDebugLayers.preAdminRoadFinalization = finalizePreAdminRoadTopology();
markFeatureTiming("road-finalization");
function completeLargePatchHumanDensity() {
const polygon = Array.isArray(options?.patchHumanFocusPolygon) ? options.patchHumanFocusPolygon : null;
const referenceDensity = Number(options?.patchTargetSettlementDensityPer1000);
const debug = {
enabled: largePatchTile && !!polygon?.length && Number.isFinite(referenceDensity),
focusLandCells: 0,
existingSettlements: 0,
targetSettlements: 0,
addedVillages: 0,
finalSettlements: 0,
};
if (!debug.enabled || polygon.length < 3 || referenceDensity <= 0) return debug;
const pointInsideFocus = (x, y) => {
const px = x + 0.5;
const py = y + 0.5;
let insideFocus = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const a = polygon[i];
const b = polygon[j];
if (!Number.isFinite(a?.x) || !Number.isFinite(a?.y) || !Number.isFinite(b?.x) || !Number.isFinite(b?.y)) continue;
const intersects = ((a.y > py) !== (b.y > py))
&& (px < (b.x - a.x) * (py - a.y) / (b.y - a.y) + a.x);
if (intersects) insideFocus = !insideFocus;
}
return insideFocus;
};
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const i = indexOf(x, y);
if (!sea[i] && pointInsideFocus(x, y)) debug.focusLandCells++;
}
}
const settlementLayers = [villages, markets, modernCities, satelliteCities, newTowns, ports];
const occupied = [];
for (const layer of settlementLayers) {
for (const point of layer || []) {
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) continue;
occupied.push(point);
if (pointInsideFocus(point.x, point.y) && !sea[indexOf(Math.round(point.x), Math.round(point.y))]) {
debug.existingSettlements++;
}
}
}
const terrainType = terrain?.terrainTemplate?.terrainType || terrain?.terrainDebug?.terrainType || "auto";
const densityFactor = terrainType === "oceanic_archipelago" ? 0.65
: terrainType === "setouchi_inland_sea" ? 0.85
: 1;
// Only previously ungenerated cells are part of an Expansion candidate's
// human-quality denominator. The raw helper does not carry the full world,
// so the coordinator supplies this immutable pre-operation fraction. A
// small margin absorbs coastline/ownership changes during final merge.
const expansionFraction = clamp(Number.isFinite(options?.patchHumanExpansionFraction)
? options.patchHumanExpansionFraction : 1, 0, 1);
const finalizationMargin = 1.08;
const effectiveFocusLandCells = debug.focusLandCells * expansionFraction;
debug.expansionFraction = expansionFraction;
debug.effectiveFocusLandCells = effectiveFocusLandCells;
// A canonical tile can still be present when only a very small sliver is
// newly generated. Apply the same small-area exception to the effective
// ungenerated area, not the complete raw focus frame, or a near-zero
// Expansion fraction would manufacture the two-settlement floor.
debug.targetSettlements = effectiveFocusLandCells < 180
? 0
: Math.max(2, Math.floor(effectiveFocusLandCells * referenceDensity * densityFactor * finalizationMargin / 1000));
let deficit = Math.max(0, debug.targetSettlements - debug.existingSettlements);
if (!deficit) {
debug.finalSettlements = debug.existingSettlements;
return debug;
}
const candidates = [];
for (let y = 1; y < MAP_H - 1; y++) {
for (let x = 1; x < MAP_W - 1; x++) {
if (!pointInsideFocus(x, y)) continue;
const i = indexOf(x, y);
if (sea[i] || slope[i] > 0.46 || ridgeField[i] > 0.72) continue;
const score = Math.max(villageScore[i] || 0, openPlainVillageScore[i] || 0)
+ (ruralSuitability[i] || 0) * 0.16
+ (plain[i] || 0) * 0.08
+ (agriculture[i] || 0) * 0.08
- (river[i] || 0) * 0.05;
if (score < 0.16) continue;
candidates.push({ x, y, i, score });
}
}
candidates.sort((a, b) => b.score - a.score || a.y - b.y || a.x - b.x);
const tooClose = (x, y, minDistance) => {
const d2 = minDistance * minDistance;
for (const point of occupied) {
const dx = Number(point.x) - x;
const dy = Number(point.y) - y;
if (dx * dx + dy * dy < d2) return true;
}
return false;
};
const addCandidate = (candidate) => {
const population = Math.round((900
+ Math.pow(rand(seed, 28900 + candidate.x * 31 + candidate.y * 17), 1.22) * 7600
+ (ruralSuitability[candidate.i] || 0) * 3600
+ (agriculture[candidate.i] || 0) * 2800) / 100) * 100;
const point = {
x: candidate.x,
y: candidate.y,
regionId: regionIdAt(candidate.x, candidate.y),
kind: (plain[candidate.i] || 0) > 0.30 ? "Plain Village" : "Village",
population,
score: candidate.score,
patchHumanDensityInfill: true,
};
villages.push(point);
occupied.push(point);
debug.addedVillages++;
deficit--;
};
// Preserve the initial generator's ordinary village spacing where possible;
// a tighter second pass is only a bounded fallback for thin/oblique lasso
// fragments whose usable land cannot satisfy the same density otherwise.
for (const minDistance of [5.75, 4.5]) {
if (deficit <= 0) break;
for (const candidate of candidates) {
if (deficit <= 0) break;
if (tooClose(candidate.x, candidate.y, minDistance)) continue;
addCandidate(candidate);
}
}
debug.finalSettlements = debug.existingSettlements + debug.addedVillages;
if (debug.addedVillages > 0) {
// The expensive transport skeleton is intentionally already complete.
// Recompute the rural influence consumed by the raster land-use stage so
// these full-production villages participate in population/land-use and
// later administration/naming rather than existing as display-only labels.
villageInfluence = influenceFromPoints(villages, 6, (v) => clamp((v.population || 1800) / 4200, 0.35, 1.2));
}
return debug;
}
transportDebugLayers.patchHumanDensityInfill = completeLargePatchHumanDensity();
markFeatureTiming("patch-human-density-infill");
const finalRoadInfluencePaths = [...nationalRoads, ...ringRoads, ...externalRoads];
roadLanduseInfluence = cachedInfluenceFromPaths(finalRoadInfluencePaths, 2.25, "road:landuse:final");
roadInfluence = cachedInfluenceFromPaths(finalRoadInfluencePaths, 5.0, "road:influence:final");

View file

@ -521,6 +521,7 @@ export function finishMapOutput({
height: MAP_H,
seed,
});
outputProgress("municipality coherence");
if (adminDebug) adminDebug.municipalCoherence = municipalCoherence.debug;
const coherentMunicipalityToPrefectureId = municipalCoherence.municipalityToPrefectureId || municipalityToPrefectureId;
const adminCenters = attachIdsAndNames(tagInsidePrefecture(municipalCoherence.adminCenters, humanRegionMask), "admin", seed, "Municipal Center", nameFields, usedNames, nameDebug);
@ -608,6 +609,7 @@ export function finishMapOutput({
...satelliteCities,
...newTowns,
]);
outputProgress("population packaging");
function addMunicipalCenterLocalAccess() {
const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = { repairedSegments: [], unservedSettlements: [] })) : { repairedSegments: [], unservedSettlements: [] };
@ -682,6 +684,7 @@ export function finishMapOutput({
let goal = -1;
let expanded = 0;
while (heap.length && expanded++ < maxExpanded) {
if ((expanded & 2047) === 0) outputProgress(`municipal road search ${expanded}/${maxExpanded}`);
const current = heap.pop();
if (!current || closed[current.i]) continue;
closed[current.i] = 1;
@ -739,7 +742,9 @@ export function finishMapOutput({
}) ? path : [];
}
for (const center of candidates) {
for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
const center = candidates[candidateIndex];
if ((candidateIndex & 3) === 0) outputProgress(`municipal road ${candidateIndex + 1}/${candidates.length}`);
const path = routeAccess(center);
debugLayers.unservedSettlements.push({ x: center.x, y: center.y, kind: "Municipal Center", mode: "municipal-access", repaired: path.length >= 4 });
if (path.length < 4) continue;
@ -859,6 +864,7 @@ export function finishMapOutput({
if (!current) break;
const cur = current.i;
expanded++;
if ((expanded & 2047) === 0) outputProgress(`road component search ${expanded}/${maxExpanded}`);
if (mainMask[cur] && dist[cur] > 2) { goal = cur; break; }
const [x, y] = xyOf(cur);
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
@ -914,7 +920,10 @@ export function finishMapOutput({
}
}
const centroid = { x: sx / Math.max(1, sn), y: sy / Math.max(1, sn) };
for (const comp of comps.slice(1, 24)) {
const connectableComponents = comps.slice(1, 24);
for (let componentIndex = 0; componentIndex < connectableComponents.length; componentIndex++) {
const comp = connectableComponents[componentIndex];
if ((componentIndex & 3) === 0) outputProgress(`road component ${componentIndex + 1}/${connectableComponents.length}`);
if (!componentNearAdminCenter(comp)) continue;
const land = majorityLandId(comp.cells, landIds);
if (land !== mainLand) { result.skippedIsland++; continue; }
@ -1080,9 +1089,13 @@ export function finishMapOutput({
// Build required municipal access before pruning so the prune pass can
// preserve those paths directly instead of deleting and re-adding them.
addMunicipalCenterLocalAccess();
outputProgress("municipal road access");
const requiredStubsAdded = ensureAdminCenterRoadStubs();
outputProgress("required road stubs");
const endpointConnectorsAdded = connectNearbyRoadEndpoints();
outputProgress("road endpoint connectors");
const prune = pruneIsolatedFinalRoadComponents();
outputProgress("road component pruning");
const finalComponents = occupancyComponentsFromPathGroups(
[minorRoads, nationalRoads, externalRoads, ringRoads, expressways, externalExpressways],
{ maxDistanceSq: 5, accept: (_x, _y, i) => !sea[i] }
@ -1147,15 +1160,18 @@ export function finishMapOutput({
return renamed;
}
renameInterchangesFromMunicipalities();
outputProgress("interchange naming");
nameDebug.maxDerivedPerBase = 0;
const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug, { modernCities, adminCenters });
outputProgress("prefecture regions");
const regionalPrefectureBorders = adminRegionalPrefectureBorders || [];
// Municipal vectors are derived only after final prefecture IDs exist. This
// prevents prefecture edges from also being emitted as municipal borders in
// the initial map. The stage-level vectors remain useful during generation,
// but are not authoritative output data.
const adminBorders = extractAdminBorderSegments(adminId, humanRegionMask, prefectureRegionId, sea);
outputProgress("administrative borders");
if (adminDebug) {
adminDebug.stageMunicipalBorderCount = Array.isArray(stageAdminBorders) ? stageAdminBorders.length : 0;
adminDebug.finalMunicipalBorderCount = adminBorders.length;

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,79 @@
export function createPatchContext({ world, rects, candidateWindow, seed, patchAlpha, sourceIndexForWorld, worldIndex }) {
export function createPatchContext({
world,
rects,
candidateWindow,
seed,
patchAlpha,
patchContinuousBlendAlpha,
patchCellIsNew,
generatedFootprintAlpha = 0.72,
sourceIndexForWorld,
worldIndex,
}) {
const writeRect = rects?.writeRect;
const writeCells = [];
let cellColumns = null;
if (world && writeRect && typeof patchAlpha === "function" && typeof sourceIndexForWorld === "function" && typeof worldIndex === "function") {
const capacity = Math.max(0, (writeRect.x1 - writeRect.x0) * (writeRect.y1 - writeRect.y0));
const xValues = new Int32Array(capacity);
const yValues = new Int32Array(capacity);
const worldIndices = new Int32Array(capacity);
const sourceIndices = new Int32Array(capacity);
const alphaValues = new Float32Array(capacity);
const blendAlphaValues = new Float32Array(capacity);
const newOwnedValues = new Uint8Array(capacity);
let count = 0;
let unmappedActiveCells = 0;
let unmappedMinX = Infinity, unmappedMinY = Infinity, unmappedMaxX = -Infinity, unmappedMaxY = -Infinity;
const unmappedSamples = [];
for (let y = writeRect.y0; y < writeRect.y1; y++) {
for (let x = writeRect.x0; x < writeRect.x1; x++) {
const wi = worldIndex(world, x, y);
if (wi < 0) continue;
const si = sourceIndexForWorld(rects, candidateWindow, x, y);
if (si < 0) continue;
const alpha = patchAlpha(x, y, rects, seed);
if (alpha <= 0.005) continue;
writeCells.push({ x, y, wi, si, alpha });
const si = sourceIndexForWorld(rects, candidateWindow, x, y);
if (si < 0) {
unmappedActiveCells++;
unmappedMinX = Math.min(unmappedMinX, x);
unmappedMinY = Math.min(unmappedMinY, y);
unmappedMaxX = Math.max(unmappedMaxX, x);
unmappedMaxY = Math.max(unmappedMaxY, y);
if (unmappedSamples.length < 8) unmappedSamples.push({ x, y, alpha });
continue;
}
xValues[count] = x;
yValues[count] = y;
worldIndices[count] = wi;
sourceIndices[count] = si;
alphaValues[count] = alpha;
blendAlphaValues[count] = typeof patchContinuousBlendAlpha === "function"
? patchContinuousBlendAlpha(rects, x, y, seed)
: alpha;
newOwnedValues[count] = typeof patchCellIsNew === "function" && patchCellIsNew(rects, x, y) && alpha >= generatedFootprintAlpha ? 1 : 0;
count++;
}
}
cellColumns = {
count,
xValues,
yValues,
worldIndices,
sourceIndices,
alphaValues,
blendAlphaValues,
newOwnedValues,
unmappedActiveCells,
unmappedActiveBounds: unmappedActiveCells ? {
x0: unmappedMinX, y0: unmappedMinY, x1: unmappedMaxX + 1, y1: unmappedMaxY + 1,
} : null,
unmappedSamples,
};
}
return {
world,
rects,
candidateWindow,
seed,
writeCells,
cellColumns,
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import { CELL_SIZE, MAP_H, MAP_W, indexOf, nowMs } from "./mapUtils.js";
import { finalizeRectTerrainForFixedMap, generateTerrainAndRivers, generateTerrainRect } from "./mapTerrain.js";
import { generateTerrainAndRivers } from "./mapTerrain.js";
import { generateMapFeatures } from "./mapFeatures.js";
import { finishMapOutput } from "./mapOutput.js";
import { generateAdminLayout } from "./mapAdminStage.js";
@ -91,60 +91,12 @@ function makeRuntimeOptions(options, baseSeed) {
};
}
function localizeWorldNativePath(path, originX, originY) {
if (!Array.isArray(path)) return path;
const out = path.map((tuple) => Array.isArray(tuple) && tuple.length >= 2
? [tuple[0] - originX, tuple[1] - originY, ...tuple.slice(2)]
: tuple);
for (const key of Object.keys(path)) {
if (!/^\d+$/.test(key)) out[key] = path[key];
}
return out;
}
function localizeWorldNativeTerrainPaths(terrain, originX, originY) {
const out = { ...terrain };
for (const key of ["mainRivers", "tributaryRivers", "smallStreams", "riverPaths"]) {
if (!Array.isArray(terrain?.[key])) continue;
out[key] = terrain[key].map((path) => localizeWorldNativePath(path, originX, originY));
}
return out;
}
function generateStableWorldTerrain(options = {}) {
const originX = Math.floor(Number.isFinite(options.originX) ? options.originX : 0);
const originY = Math.floor(Number.isFinite(options.originY) ? options.originY : 0);
// Keep terrain seed independent of candidate origin/window. Variant remains an
// intentional alternate-world input, while selection geometry does not alter
// terrain at a fixed absolute coordinate.
const terrainSeed = Number.isFinite(options.stableTerrainSeed)
? options.stableTerrainSeed >>> 0
: Number.isFinite(options.baseSeed) ? options.baseSeed >>> 0 : 0;
const terrain = generateTerrainRect({
...options,
seed: terrainSeed,
variant: options.variant || 0,
originX,
originY,
width: MAP_W,
height: MAP_H,
seaLevel: Number.isFinite(options.worldSeaLevel) ? options.worldSeaLevel : undefined,
name: "world-native-patch-terrain",
});
const finalized = finalizeRectTerrainForFixedMap(terrainSeed, terrain, options);
return localizeWorldNativeTerrainPaths(finalized, originX, originY);
}
function generateInitialTerrain(seed, options = {}) {
// Expansion may supply a quality-selected production terrain override
// candidate selector. It is fed through exactly the same geography,
// settlement, administration, transport, and output stages as initial
// generation; only the terrain search is performed ahead of time.
if (options.terrainOverride) return options.terrainOverride;
// Regeneration retains the legacy production terrain. Stable rect terrain is
// still available for deterministic diagnostics and compatibility, but normal
// expansion candidates now select from production terrain candidates.
if (options.stableWorldTerrain === true) return generateStableWorldTerrain(options);
return generateTerrainAndRivers(seed, options);
}

View file

@ -10,6 +10,52 @@ import { createRectContext, createRectTerrainFields, rectIndexOf, rectInside, re
const ASPECT = MAP_W / MAP_H;
const SQRT2 = Math.SQRT2;
export function createExactNoiseMemo() {
const latticeBySeed = new Map();
const lattice = (x, y, seed) => {
let cache = latticeBySeed.get(seed);
if (!cache) {
cache = new Map();
latticeBySeed.set(seed, cache);
}
// Terrain noise lattice coordinates stay far inside this stride. Using one
// numeric key avoids allocating a string for every hot-loop lookup.
const key = x * 131072 + y;
const cached = cache.get(key);
if (cached !== undefined) return cached;
const value = hash2(x, y, seed);
cache.set(key, value);
return value;
};
const memoValueNoise = (x, y, seed, scale) => {
const sx = x / scale;
const sy = y / scale;
const x0 = Math.floor(sx);
const y0 = Math.floor(sy);
const tx = smoothstep(sx - x0);
const ty = smoothstep(sy - y0);
const a = lattice(x0, y0, seed);
const b = lattice(x0 + 1, y0, seed);
const c = lattice(x0, y0 + 1, seed);
const d = lattice(x0 + 1, y0 + 1, seed);
return lerp(lerp(a, b, tx), lerp(c, d, tx), ty);
};
const memoFbm = (x, y, seed) => {
let amp = 1;
let scale = 54;
let sum = 0;
let norm = 0;
for (let octave = 0; octave < 5; octave++) {
sum += memoValueNoise(x, y, seed + octave * 101, scale) * amp;
norm += amp;
amp *= 0.5;
scale *= 0.5;
}
return sum / norm;
};
return { valueNoise: memoValueNoise, fbm: memoFbm, latticeBySeed };
}
function normalizeCoord(x, y) {
return {
px: (x + 0.5) / MAP_W,
@ -95,50 +141,82 @@ function largestComponent(mask, allowEdgePreference = false) {
}
function distanceField(sourceMask, maxDistance = 999) {
const dist = new Float32Array(SIZE);
dist.fill(maxDistance);
const heap = new MinHeap();
for (let i = 0; i < SIZE; i++) {
if (!sourceMask[i]) continue;
dist[i] = 0;
heap.push({ i, f: 0 });
}
while (heap.length) {
const cur = heap.pop();
if (!cur || cur.f > dist[cur.i] + 1e-5) continue;
const x = cur.i % MAP_W;
const y = Math.floor(cur.i / MAP_W);
for (const [nx, ny] of neighbors8(x, y)) {
const ni = indexOf(nx, ny);
const step = (nx !== x && ny !== y) ? SQRT2 : 1;
const nd = cur.f + step;
if (nd >= dist[ni] || nd > maxDistance) continue;
dist[ni] = nd;
heap.push({ i: ni, f: nd });
// With no obstacles, the former Dijkstra computes the exact 8-neighbour
// octile distance to the nearest source. Two causal raster passes compute
// the same metric in O(cells) instead of O(cells log cells). Keep a Float64
// work buffer so the final Float32 values are bit-identical to the heap path
// (verified against randomized masks and production candidates).
const work = new Float64Array(SIZE);
work.fill(maxDistance);
for (let i = 0; i < SIZE; i++) if (sourceMask[i]) work[i] = 0;
for (let y = 0; y < MAP_H; y++) {
const row = y * MAP_W;
for (let x = 0; x < MAP_W; x++) {
const i = row + x;
let value = work[i];
if (x > 0) value = Math.min(value, work[i - 1] + 1);
if (y > 0) {
value = Math.min(value, work[i - MAP_W] + 1);
if (x > 0) value = Math.min(value, work[i - MAP_W - 1] + SQRT2);
if (x + 1 < MAP_W) value = Math.min(value, work[i - MAP_W + 1] + SQRT2);
}
work[i] = value > maxDistance ? maxDistance : value;
}
}
for (let y = MAP_H - 1; y >= 0; y--) {
const row = y * MAP_W;
for (let x = MAP_W - 1; x >= 0; x--) {
const i = row + x;
let value = work[i];
if (x + 1 < MAP_W) value = Math.min(value, work[i + 1] + 1);
if (y + 1 < MAP_H) {
value = Math.min(value, work[i + MAP_W] + 1);
if (x + 1 < MAP_W) value = Math.min(value, work[i + MAP_W + 1] + SQRT2);
if (x > 0) value = Math.min(value, work[i + MAP_W - 1] + SQRT2);
}
work[i] = value > maxDistance ? maxDistance : value;
}
}
const dist = new Float32Array(SIZE);
for (let i = 0; i < SIZE; i++) dist[i] = work[i];
return dist;
}
function ridgeContribution(px, py, ridge, seed) {
function ridgeContribution(px, py, ridge, seed, noise = null) {
const dx = (px - ridge.x) * ASPECT;
const dy = py - ridge.y;
const { u, v } = rotate(dx, dy, ridge.angle);
const c = ridge._angleCos ?? Math.cos(ridge.angle);
const s = ridge._angleSin ?? Math.sin(ridge.angle);
const u = dx * c + dy * s;
const v = -dx * s + dy * c;
const half = ridge.length * 0.5;
const along = Math.abs(u / Math.max(0.001, half));
if (along >= 1.22) return 0;
const taper = smoothstep(1 - clamp((along - 0.68) / 0.54));
const wobble = (valueNoise((u + ridge.phase) * 720, (v + ridge.phase * 0.37) * 980, seed + ridge.seedOffset, 14) - 0.5) * ridge.width * ridge.wobble;
// Before evaluating two noise fields, reject only ridges whose Gaussian is
// guaranteed to underflow to exact zero for every possible wobble value.
// This is an exact fast path: exp(-28^2) is zero in IEEE-754 binary64, so the
// original contribution would also be exactly zero.
const maxWobbleDistance = ridge.width * ridge.wobble * 0.5;
if (Math.abs(v) - maxWobbleDistance > ridge.width * 28) return 0;
const valueNoiseFn = noise?.valueNoise || valueNoise;
const wobble = (valueNoiseFn((u + ridge.phase) * 720, (v + ridge.phase * 0.37) * 980, seed + ridge.seedOffset, 14) - 0.5) * ridge.width * ridge.wobble;
const cross = Math.abs(v + wobble);
const core = Math.exp(-Math.pow(cross / Math.max(0.0008, ridge.width), 2.0));
const serration = 0.82 + 0.36 * valueNoise((u + ridge.phase) * 900, (v - ridge.phase * 0.41) * 1200, seed + ridge.seedOffset + 71, 7.5);
const serration = 0.82 + 0.36 * valueNoiseFn((u + ridge.phase) * 900, (v - ridge.phase * 0.41) * 1200, seed + ridge.seedOffset + 71, 7.5);
return ridge.height * core * taper * serration;
}
function ellipticalMask(px, py, system) {
const dx = (px - system.x) * ASPECT;
const dy = py - system.y;
const { u, v } = rotate(dx, dy, system.angle);
const c = system._angleCos ?? Math.cos(system.angle);
const s = system._angleSin ?? Math.sin(system.angle);
const u = dx * c + dy * s;
const v = -dx * s + dy * c;
const a = Math.max(0.01, system.length * 0.5);
const along = clamp((u / a + 1) * 0.5);
const widthWave = 1
@ -602,13 +680,103 @@ function buildScratchRidges(system, seed, systemId) {
return ridges;
}
function computeCoastLower(px, py, template, seed) {
function buildRidgeBucketIndex(ridges, terrainFrameScale, terrainFrameOffsetX, terrainFrameOffsetY, bucketSize = 4) {
const columns = Math.ceil(MAP_W / bucketSize);
const rows = Math.ceil(MAP_H / bucketSize);
const buckets = Array.from({ length: columns * rows }, () => []);
const frameX = (x) => 0.5 + (((x + 0.5) / MAP_W) - 0.5) / terrainFrameScale + terrainFrameOffsetX;
const frameY = (y) => 0.5 + (((y + 0.5) / MAP_H) - 0.5) / terrainFrameScale + terrainFrameOffsetY;
for (const ridge of ridges) {
const c = ridge._angleCos;
const s = ridge._angleSin;
const limit = Math.max(0.001, ridge.length * 0.5) * 1.22;
// ridgeContribution returns exact zero before evaluating noise whenever
// the cross-axis distance exceeds this conservative bound. Apply the same
// exact predicate to a whole bucket; linear u/v extrema occur at corners.
const crossLimit = ridge.width * ridge.wobble * 0.5 + ridge.width * 28;
for (let by = 0; by < rows; by++) {
const y0 = by * bucketSize;
const y1 = Math.min(MAP_H - 1, y0 + bucketSize - 1);
const py0 = frameY(y0);
const py1 = frameY(y1);
for (let bx = 0; bx < columns; bx++) {
const x0 = bx * bucketSize;
const x1 = Math.min(MAP_W - 1, x0 + bucketSize - 1);
const px0 = frameX(x0);
const px1 = frameX(x1);
const u00 = (px0 - ridge.x) * ASPECT * c + (py0 - ridge.y) * s;
const u10 = (px1 - ridge.x) * ASPECT * c + (py0 - ridge.y) * s;
const u01 = (px0 - ridge.x) * ASPECT * c + (py1 - ridge.y) * s;
const u11 = (px1 - ridge.x) * ASPECT * c + (py1 - ridge.y) * s;
const v00 = -(px0 - ridge.x) * ASPECT * s + (py0 - ridge.y) * c;
const v10 = -(px1 - ridge.x) * ASPECT * s + (py0 - ridge.y) * c;
const v01 = -(px0 - ridge.x) * ASPECT * s + (py1 - ridge.y) * c;
const v11 = -(px1 - ridge.x) * ASPECT * s + (py1 - ridge.y) * c;
const minU = Math.min(u00, u10, u01, u11);
const maxU = Math.max(u00, u10, u01, u11);
const minV = Math.min(v00, v10, v01, v11);
const maxV = Math.max(v00, v10, v01, v11);
if (minU <= limit + 1e-12 && maxU >= -limit - 1e-12
&& minV <= crossLimit + 1e-12 && maxV >= -crossLimit - 1e-12) {
buckets[by * columns + bx].push(ridge);
}
}
}
}
return { bucketSize, columns, buckets };
}
function buildMountainSystemBucketIndex(systems, terrainFrameScale, terrainFrameOffsetX, terrainFrameOffsetY, bucketSize = 4) {
const columns = Math.ceil(MAP_W / bucketSize);
const rows = Math.ceil(MAP_H / bucketSize);
const buckets = Array.from({ length: columns * rows }, () => []);
const frameX = (x) => 0.5 + (((x + 0.5) / MAP_W) - 0.5) / terrainFrameScale + terrainFrameOffsetX;
const frameY = (y) => 0.5 + (((y + 0.5) / MAP_H) - 0.5) / terrainFrameScale + terrainFrameOffsetY;
for (const system of systems) {
const c = system._angleCos;
const s = system._angleSin;
const uLimit = Math.max(0.01, system.length * 0.5) * 1.2;
const vLimit = Math.max(0.012, system.width * 0.5 * 1.6) * 1.2;
for (let by = 0; by < rows; by++) {
const y0 = by * bucketSize;
const y1 = Math.min(MAP_H - 1, y0 + bucketSize - 1);
const py0 = frameY(y0);
const py1 = frameY(y1);
for (let bx = 0; bx < columns; bx++) {
const x0 = bx * bucketSize;
const x1 = Math.min(MAP_W - 1, x0 + bucketSize - 1);
const px0 = frameX(x0);
const px1 = frameX(x1);
let minU = Infinity, maxU = -Infinity, minV = Infinity, maxV = -Infinity;
for (const [px, py] of [[px0, py0], [px1, py0], [px0, py1], [px1, py1]]) {
const dx = (px - system.x) * ASPECT;
const dy = py - system.y;
const u = dx * c + dy * s;
const v = -dx * s + dy * c;
minU = Math.min(minU, u); maxU = Math.max(maxU, u);
minV = Math.min(minV, v); maxV = Math.max(maxV, v);
}
if (minU <= uLimit + 1e-12 && maxU >= -uLimit - 1e-12
&& minV <= vLimit + 1e-12 && maxV >= -vLimit - 1e-12) {
buckets[by * columns + bx].push(system);
}
}
}
}
return { bucketSize, columns, buckets };
}
function computeCoastLower(px, py, template, seed, noise = null) {
const valueNoiseFn = noise?.valueNoise || valueNoise;
const fbmFn = noise?.fbm || fbm;
const angle = template.coastAngle;
const axis = (px - 0.5) * Math.cos(angle) * ASPECT + (py - 0.5) * Math.sin(angle);
const cross = -(px - 0.5) * Math.sin(angle) * ASPECT + (py - 0.5) * Math.cos(angle);
const wave = (fbm(px * 220, py * 220, seed + 300) - 0.5) * template.coastNoise;
const bay = (valueNoise(px * 500, py * 500, seed + 301, 22) - 0.5) * 0.055;
const islandNoise = (fbm(px * 420 + 17, py * 420 - 11, seed + 302) - 0.5) * 0.032;
const c = template._coastAngleCos ?? Math.cos(angle);
const s = template._coastAngleSin ?? Math.sin(angle);
const axis = (px - 0.5) * c * ASPECT + (py - 0.5) * s;
const cross = -(px - 0.5) * s * ASPECT + (py - 0.5) * c;
const wave = (fbmFn(px * 220, py * 220, seed + 300) - 0.5) * template.coastNoise;
const bay = (valueNoiseFn(px * 500, py * 500, seed + 301, 22) - 0.5) * 0.055;
const islandNoise = (fbmFn(px * 420 + 17, py * 420 - 11, seed + 302) - 0.5) * 0.032;
let pressure = 0;
if (template.coastStyle === "oceanic_archipelago") {
@ -651,7 +819,7 @@ function computeCoastLower(px, py, template, seed) {
pressure = Math.max(sideA, sideB, outerBite);
}
return { pressure: clamp(pressure), signedAxis: axis };
return clamp(pressure);
}
function recomputeSlope(elevation, sea, slope) {
@ -1094,7 +1262,7 @@ function buildRiverNetwork(seed, template, sea, lake, elevation, slope, flowTo,
return { riverPaths: sortedPaths.slice(0, 80), mainRivers, tributaryRivers, smallStreams };
}
function deriveFields(seed, template, fields, seaLevel) {
function deriveFields(seed, template, fields, seaLevel, progress = null) {
const {
elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture,
ridgeField, valleyField, visibleRavineField, surfaceTextureField, basinField,
@ -1104,14 +1272,18 @@ function deriveFields(seed, template, fields, seaLevel) {
} = fields;
recomputeSlope(elevation, sea, slope);
progress?.("derived slope reconstructed");
const waterDist = distanceField(sea, 80);
progress?.("derived coast distance field complete");
const riverMask = new Uint8Array(SIZE);
for (let i = 0; i < SIZE; i++) if (river[i] > 0.18) riverMask[i] = 1;
const riverDist = distanceField(riverMask, 40);
progress?.("derived river distance field complete");
const landElevationValues = [];
for (let i = 0; i < SIZE; i++) if (!sea[i]) landElevationValues.push(elevation[i]);
const lowlandQuantile = template.terrainType === "chubu_mountain" || template.terrainType === "tohoku_spine" ? 0.24 : 0.31;
const lowlandElevationCeiling = quantile(landElevationValues, lowlandQuantile);
progress?.("derived lowland quantile complete");
for (let y = 1; y < MAP_H - 1; y++) {
for (let x = 1; x < MAP_W - 1; x++) {
@ -1154,6 +1326,7 @@ function deriveFields(seed, template, fields, seaLevel) {
passSuitability[i] = clamp(slope[i] * 0.30 + valleyField[i] * 0.34 + clamp((0.75 - ridgeField[i]) * 0.8) + plain[i] * 0.18);
moisture[i] = clamp(0.30 + (1 - waterDist[i] / 65) * 0.36 + valleyField[i] * 0.22 + riverNear * 0.26 - Math.max(0, e - 0.55) * 0.36 + (fbm(x * 1.6, y * 1.6, seed + 15000) - 0.5) * 0.18);
}
if (y % 24 === 23 || y + 2 === MAP_H) progress?.(`derived fields ${y}/${MAP_H - 2} rows`);
}
for (let i = 0; i < SIZE; i++) {
if (!sea[i]) continue;
@ -1500,12 +1673,33 @@ function buildRectNaturalRegions(ctx, fields, seed, template) {
seeds.push({
wx,
wy,
gx,
gy,
id: 40000000 + rectStableId(seed, gx, gy, 0xb5c0fbcf) % 42000000,
coarseId: 30000000 + rectStableId(seed, Math.floor((gx * spacing) / coarseSpacing), Math.floor((gy * spacing) / coarseSpacing), 0xc2b2ae35) % 42000000,
});
}
}
if (!seeds.length) return { naturalCompartmentCount: 0, regionCount: 0 };
// Seeds come from a regular world grid with bounded jitter. Searching every
// seed for every cell made this stage O(cells x all world-window seeds), even
// though a seed outside the surrounding 5x5 grid cannot be geographically
// relevant. The bucket lookup keeps the same stable world anchoring while
// bounding the hot loop to nearby candidates.
const seedsByGrid = new Map();
for (const item of seeds) seedsByGrid.set(`${item.gx},${item.gy}`, item);
function nearbySeeds(wx, wy) {
const centerGX = Math.floor(wx / spacing);
const centerGY = Math.floor(wy / spacing);
const nearby = [];
for (let gy = centerGY - 2; gy <= centerGY + 2; gy++) {
for (let gx = centerGX - 2; gx <= centerGX + 2; gx++) {
const item = seedsByGrid.get(`${gx},${gy}`);
if (item) nearby.push(item);
}
}
return nearby.length ? nearby : seeds;
}
for (let y = 0; y < ctx.height; y++) {
for (let x = 0; x < ctx.width; x++) {
const i = rectIndexOf(ctx, x, y);
@ -1516,13 +1710,18 @@ function buildRectNaturalRegions(ctx, fields, seed, template) {
let bestScore = Infinity;
const barrier = (naturalBarrierScore[i] || 0) + (ridgeField[i] || 0) * 0.55 + (flowAccum[i] || 0) * 0.18;
const basinBonus = (basinField[i] || 0) * 0.18 + (valleyField[i] || 0) * 0.10;
for (const s of seeds) {
const cellAdjustment = barrier * spacing * 0.42 - basinBonus * spacing * 0.32;
for (const s of nearbySeeds(wx, wy)) {
const dx = (wx - s.wx) * 1.05;
const dy = wy - s.wy;
const d = Math.hypot(dx, dy);
// tileNoise is bounded to +/- 0.17 * spacing and the watershed term is
// non-negative. Avoid the comparatively expensive smooth-noise lookup
// when even the candidate's theoretical lower bound cannot win.
if (d + cellAdjustment - spacing * 0.17 > bestScore) continue;
const tileNoise = (valueNoise(wx + s.wx * 0.13, wy + s.wy * 0.13, seed ^ 0xa54ff53a, 52) - 0.5) * spacing * 0.34;
const watershedPenalty = watershedId?.[i] >= 0 ? ((watershedId[i] ^ s.id) & 7) * 0.16 : 0;
const score = d + barrier * spacing * 0.42 - basinBonus * spacing * 0.32 + tileNoise + watershedPenalty;
const score = d + cellAdjustment + tileNoise + watershedPenalty;
if (score < bestScore) { bestScore = score; best = s; }
}
naturalCompartmentId[i] = best.id;
@ -1688,6 +1887,13 @@ export function generateTerrainRect(options = {}) {
visibleRavineField, surfaceTextureField,
} = fields;
const progress = (key, label) => options.onProgress?.({
status: "start",
key: `terrain-rect:${key}`,
label: `Rect terrain: ${label}`,
});
progress("base", "base elevation and climate");
for (let y = 0; y < ctx.height; y++) {
for (let x = 0; x < ctx.width; x++) {
const i = rectIndexOf(ctx, x, y);
@ -1730,14 +1936,22 @@ export function generateTerrainRect(options = {}) {
0.13,
terrainTemplate.terrainType === "oceanic_archipelago" ? 0.72 : 0.50
);
progress("water", "water classification");
const oceanCells = classifyRectWater(ctx, fields, seaLevel);
progress("slope", "slope reconstruction");
recomputeRectSlope(ctx, fields);
progress("drainage", "priority flood and drainage");
const filled = priorityFloodRect(ctx, fields);
computeRectFlowAccumulation(ctx, fields, filled);
progress("watersheds", "watershed assignment");
const watershedDebug = buildRectWatershedId(ctx, fields, rectSeedValue ^ 0x51ed270b);
progress("derived", "derived terrain fields");
deriveRectTerrainFields(ctx, fields, seaLevel);
progress("rivers", "river paths");
const riverNetwork = buildRectRiverPaths(ctx, fields, rectSeedValue ^ 0x1f123bb5, terrainTemplate);
progress("regions", "natural regions");
const naturalDebug = buildRectNaturalRegions(ctx, fields, rectSeedValue ^ 0xb5c0fbcf, terrainTemplate);
options.onProgress?.({ status: "done", key: "terrain-rect", label: "Rect terrain ready" });
let landCount = 0;
let mountainCount = 0;
@ -1837,6 +2051,15 @@ export function generateTerrainAndRivers(seed, options = {}) {
const originY = Math.floor(Number.isFinite(options.originY) ? options.originY : (Number.isFinite(generationContext.originY) ? generationContext.originY : 0));
const variant = Math.max(0, Math.floor(Number.isFinite(options.variant) ? options.variant : (Number.isFinite(generationContext.variant) ? generationContext.variant : 0))) >>> 0;
const worldNative = options.worldNative === true || generationContext.worldNative === true;
const terrainProgress = (key, label, completed, total) => options.onProgress?.({
status: "terrain-step",
key: `terrain:${key}`,
phase: "terrain",
workUnitId: "terrain-production",
label,
completed,
total,
});
const fields = createMapFields();
fields.visibleRavineField = new Float32Array(SIZE);
fields.surfaceTextureField = new Float32Array(SIZE);
@ -1852,36 +2075,69 @@ export function generateTerrainAndRivers(seed, options = {}) {
const terrainTemplate = buildTerrainTemplate(seed, options);
const systems = buildMountainSystems(terrainTemplate, seed);
const allRidges = systems.flatMap((system, id) => buildScratchRidges(system, seed, id));
// These angles are invariant for the entire candidate. The old hot loop
// recomputed sin/cos for every cell x every mountain/ridge, accounting for
// millions of identical transcendental calls per production candidate.
Object.defineProperties(terrainTemplate, {
_coastAngleCos: { value: Math.cos(terrainTemplate.coastAngle), enumerable: false },
_coastAngleSin: { value: Math.sin(terrainTemplate.coastAngle), enumerable: false },
_mountainAngleCos: { value: Math.cos(terrainTemplate.mountainAngle), enumerable: false },
_mountainAngleSin: { value: Math.sin(terrainTemplate.mountainAngle), enumerable: false },
});
for (const item of [...systems, ...allRidges]) {
Object.defineProperties(item, {
_angleCos: { value: Math.cos(item.angle), enumerable: false },
_angleSin: { value: Math.sin(item.angle), enumerable: false },
});
}
const terrainFrameScale = clamp(Number.isFinite(options.terrainFrameScale) ? options.terrainFrameScale : 1, 1, 2.5);
const terrainFrameOffsetX = clamp(Number.isFinite(options.terrainFrameOffsetX) ? options.terrainFrameOffsetX : 0, -0.28, 0.28);
const terrainFrameOffsetY = clamp(Number.isFinite(options.terrainFrameOffsetY) ? options.terrainFrameOffsetY : 0, -0.24, 0.24);
const requestedTerrainFrameOffsetX = clamp(Number.isFinite(options.terrainFrameOffsetX) ? options.terrainFrameOffsetX : 0, -0.28, 0.28);
const requestedTerrainFrameOffsetY = clamp(Number.isFinite(options.terrainFrameOffsetY) ? options.terrainFrameOffsetY : 0, -0.24, 0.24);
// A world-native patch is a crop of one larger virtual terrain frame. The
// former implementation used local candidate x/y for the template frame,
// so every canonical tile restarted the coastline and mountain template at
// its own left/top edge. Adjacent full-production candidates therefore
// disagreed along straight implementation boundaries even though their noise
// fields used world coordinates. Shift the normalized frame by the candidate
// origin so the same world cell receives the same template coordinates in
// every overlapping candidate. Requested artistic offsets remain bounded;
// the world-native crop offset itself is intentionally not clamped.
const worldFrameOffsetX = worldNative ? originX / (MAP_W * terrainFrameScale) : 0;
const worldFrameOffsetY = worldNative ? originY / (MAP_H * terrainFrameScale) : 0;
const terrainFrameOffsetX = requestedTerrainFrameOffsetX + worldFrameOffsetX;
const terrainFrameOffsetY = requestedTerrainFrameOffsetY + worldFrameOffsetY;
const ridgeBucketIndex = buildRidgeBucketIndex(allRidges, terrainFrameScale, terrainFrameOffsetX, terrainFrameOffsetY);
const systemBucketIndex = buildMountainSystemBucketIndex(systems, terrainFrameScale, terrainFrameOffsetX, terrainFrameOffsetY);
const exactNoise = createExactNoiseMemo();
for (let y = 0; y < MAP_H; y++) {
const ridgeBucketRow = Math.floor(y / ridgeBucketIndex.bucketSize) * ridgeBucketIndex.columns;
const systemBucketRow = Math.floor(y / systemBucketIndex.bucketSize) * systemBucketIndex.columns;
for (let x = 0; x < MAP_W; x++) {
const i = indexOf(x, y);
const wx = originX + x;
const wy = originY + y;
const normalized = normalizeCoord(x, y);
// Expansion candidates are a crop from a larger virtual production map.
// This removes the initial generator's deliberate edge-ocean frame from
// the user's lasso boundary while retaining the same terrain system.
const px = 0.5 + (normalized.px - 0.5) / terrainFrameScale + terrainFrameOffsetX;
const py = 0.5 + (normalized.py - 0.5) / terrainFrameScale + terrainFrameOffsetY;
const terrainLarge = (fbm(wx * 0.65, wy * 0.65, seed + 1) - 0.5) * 0.23;
const terrainRegional = (valueNoise(wx * 0.8, wy * 0.8, seed + 2, 42) - 0.5) * 0.16;
const { pressure: coastPressure } = computeCoastLower(px, py, terrainTemplate, seed);
const px = 0.5 + (((x + 0.5) / MAP_W) - 0.5) / terrainFrameScale + terrainFrameOffsetX;
const py = 0.5 + (((y + 0.5) / MAP_H) - 0.5) / terrainFrameScale + terrainFrameOffsetY;
const terrainLarge = (exactNoise.fbm(wx * 0.65, wy * 0.65, seed + 1) - 0.5) * 0.23;
const terrainRegional = (exactNoise.valueNoise(wx * 0.8, wy * 0.8, seed + 2, 42) - 0.5) * 0.16;
const coastPressure = computeCoastLower(px, py, terrainTemplate, seed, exactNoise);
let e = 0.42 + terrainLarge + terrainRegional - coastPressure * (terrainTemplate.coastStrength ?? 0.96) * (0.22 + terrainTemplate.deposition * 0.040);
let mountainMaskMax = 0;
for (let s = 0; s < systems.length; s++) {
const system = systems[s];
const activeSystems = systemBucketIndex.buckets[systemBucketRow + Math.floor(x / systemBucketIndex.bucketSize)];
for (const system of activeSystems) {
const mask = ellipticalMask(px, py, system);
mountainMaskMax = Math.max(mountainMaskMax, mask);
const broad = Math.pow(mask, lerp(1.70, 1.16, system.massifness)) * system.height * lerp(0.22, 0.34, system.massifness);
e += broad;
arcSpineField[i] = Math.max(arcSpineField[i], mask * (system.role === "minor" ? 0.42 : system.role === "primary" ? 0.86 : 0.72));
}
for (const ridge of allRidges) {
const r = ridgeContribution(px, py, ridge, seed);
const activeRidges = ridgeBucketIndex.buckets[ridgeBucketRow + Math.floor(x / ridgeBucketIndex.bucketSize)];
for (const ridge of activeRidges) {
const r = ridgeContribution(px, py, ridge, seed, exactNoise);
if (r <= 0) continue;
e += r;
branchRidgeField[i] = clamp(branchRidgeField[i] + r * 5.0);
@ -1892,19 +2148,20 @@ export function generateTerrainAndRivers(seed, options = {}) {
if (terrainTemplate.terrainType === "tohoku_spine") {
const dx = (px - 0.5) * ASPECT;
const dy = py - 0.5;
const { u, v } = rotate(dx, dy, terrainTemplate.mountainAngle);
const warp = (valueNoise(wx * 0.50, wy * 0.50, seed + 504, 28) - 0.5) * 18;
macro = (fbm(u * 520 + warp, v * 1850 - warp * 0.35, seed + 500) - 0.5) * 2;
scratch = (fbm(u * 920 + warp * 0.8, v * 2300 + warp * 0.25, seed + 502) - 0.5) * 2;
const passBreak = clamp((valueNoise(u * 760 + 17, v * 1800 - 11, seed + 505, 18) - 0.62) * 2.6);
const lateralBranch = clamp((valueNoise(v * 1700 - 3, u * 540 + 5, seed + 506, 16) - 0.54) * 2.1);
const u = dx * terrainTemplate._mountainAngleCos + dy * terrainTemplate._mountainAngleSin;
const v = -dx * terrainTemplate._mountainAngleSin + dy * terrainTemplate._mountainAngleCos;
const warp = (exactNoise.valueNoise(wx * 0.50, wy * 0.50, seed + 504, 28) - 0.5) * 18;
macro = (exactNoise.fbm(u * 520 + warp, v * 1850 - warp * 0.35, seed + 500) - 0.5) * 2;
scratch = (exactNoise.fbm(u * 920 + warp * 0.8, v * 2300 + warp * 0.25, seed + 502) - 0.5) * 2;
const passBreak = clamp((exactNoise.valueNoise(u * 760 + 17, v * 1800 - 11, seed + 505, 18) - 0.62) * 2.6);
const lateralBranch = clamp((exactNoise.valueNoise(v * 1700 - 3, u * 540 + 5, seed + 506, 16) - 0.54) * 2.1);
e += lateralBranch * mountainMaskMax * 0.022;
e -= passBreak * mountainMaskMax * 0.052;
} else {
macro = (fbm(wx * terrainTemplate.macroNoiseScale * 48, wy * terrainTemplate.macroNoiseScale * 48, seed + 500) - 0.5) * 2;
scratch = (fbm(wx * 2.2, wy * 2.2, seed + 502) - 0.5) * 2;
macro = (exactNoise.fbm(wx * terrainTemplate.macroNoiseScale * 48, wy * terrainTemplate.macroNoiseScale * 48, seed + 500) - 0.5) * 2;
scratch = (exactNoise.fbm(wx * 2.2, wy * 2.2, seed + 502) - 0.5) * 2;
}
const global = (valueNoise(wx * 0.23, wy * 0.23, seed + 501, 38) - 0.5) * 2;
const global = (exactNoise.valueNoise(wx * 0.23, wy * 0.23, seed + 501, 38) - 0.5) * 2;
e += macro * terrainTemplate.macroNoiseStrength * (0.38 + mountainMaskMax * 0.80);
e += global * 0.020;
e += scratch * terrainTemplate.scratchNoiseStrength * mountainMaskMax;
@ -1918,9 +2175,9 @@ export function generateTerrainAndRivers(seed, options = {}) {
// Setouchi should read as sea-dominant, with many compact wooded island
// backbones rather than broad continental ridges. The small-massif term
// is band-limited so it forms believable islands, not one-cell speckle.
const lowHillNoise = clamp((fbm(wx * 0.95 + 17, wy * 0.95 - 23, seed + 571) - 0.39) * 2.7);
const islandMassif = clamp((fbm(wx * 1.85 - 31, wy * 1.85 + 19, seed + 573) - 0.50) * 3.4);
const islandBackbone = clamp((valueNoise(wx * 2.8 + 7, wy * 2.8 - 11, seed + 574, 9) - 0.54) * 3.2);
const lowHillNoise = clamp((exactNoise.fbm(wx * 0.95 + 17, wy * 0.95 - 23, seed + 571) - 0.39) * 2.7);
const islandMassif = clamp((exactNoise.fbm(wx * 1.85 - 31, wy * 1.85 + 19, seed + 573) - 0.50) * 3.4);
const islandBackbone = clamp((exactNoise.valueNoise(wx * 2.8 + 7, wy * 2.8 - 11, seed + 574, 9) - 0.54) * 3.2);
const coastalIslandBias = clamp(coastPressure * 0.64 + mountainMaskMax * 0.46 + lowHillNoise * 0.24);
e += lowHillNoise * 0.090;
e += islandMassif * coastalIslandBias * 0.105;
@ -1931,7 +2188,7 @@ export function generateTerrainAndRivers(seed, options = {}) {
}
if (terrainTemplate.terrainType === "oceanic_archipelago") {
// 海洋型は大きな山塊ではなく、島列を読むための低い起伏を点在させる。
const islandCore = clamp((fbm(wx * 0.82 + 41, wy * 0.82 - 29, seed + 572) - 0.46) * 2.7);
const islandCore = clamp((exactNoise.fbm(wx * 0.82 + 41, wy * 0.82 - 29, seed + 572) - 0.46) * 2.7);
const islandChain = clamp(mountainMaskMax * 0.92 + islandCore * 0.54 - coastPressure * 0.24);
e += islandChain * 0.135;
e -= clamp((coastPressure - 0.38) * 1.55) * 0.040;
@ -1944,32 +2201,47 @@ export function generateTerrainAndRivers(seed, options = {}) {
visibleRavineField[i] = clamp(Math.abs(scratch) * mountainMaskMax * 0.30 + Math.max(0, -scratch) * mountainMaskMax * 0.40);
surfaceTextureField[i] = clamp(Math.abs(macro) * 0.12 + Math.abs(scratch) * mountainMaskMax * 0.46);
valleyField[i] = clamp(Math.max(0, -scratch) * mountainMaskMax * 0.18);
moisture[i] = clamp(0.45 + coastPressure * 0.28 - elevation[i] * 0.20 + (fbm(wx * 1.1, wy * 1.1, seed + 503) - 0.5) * 0.16);
moisture[i] = clamp(0.45 + coastPressure * 0.28 - elevation[i] * 0.20 + (exactNoise.fbm(wx * 1.1, wy * 1.1, seed + 503) - 0.5) * 0.16);
}
if (y % 8 === 7 || y + 1 === MAP_H) {
terrainProgress("base-field", `Terrain base field ${y + 1}/${MAP_H} rows`, y + 1, MAP_H + 8);
}
}
terrainProgress("water", "Terrain water classification", MAP_H + 1, MAP_H + 8);
let seaLevel = quantile(elevation, terrainTemplate.seaRatio);
seaLevel = clamp(seaLevel, 0.14, terrainTemplate.terrainType === "oceanic_archipelago" ? 0.72 : 0.47);
classifyWater(elevation, seaLevel, sea, ocean, lake);
recomputeSlope(elevation, sea, slope);
terrainProgress("drainage", "Terrain drainage and watersheds", MAP_H + 2, MAP_H + 8);
const filled = new Float32Array(SIZE);
priorityFloodFlow(elevation, sea, flowTo, filled);
computeFlowAccumulation(sea, flowTo, filled, flowAccum);
const watershedId = buildWatershedId(sea, flowTo, flowAccum);
terrainProgress("rivers", "Terrain river network", MAP_H + 3, MAP_H + 8);
const { riverPaths, mainRivers, tributaryRivers, smallStreams } = buildRiverNetwork(seed, terrainTemplate, sea, lake, elevation, slope, flowTo, flowAccum, river, erosionField);
terrainProgress("derived", "Terrain derived fields", MAP_H + 4, MAP_H + 8);
enforceLandGradient(elevation, sea, seaLevel);
deriveFields(seed, terrainTemplate, fields, seaLevel);
deriveFields(seed, terrainTemplate, fields, seaLevel, (label) => {
terrainProgress("derived-detail", `Terrain ${label}`, MAP_H + 4, MAP_H + 8);
});
const prefectureMask = makePrefectureMask(seed, sea, elevation, slope, river);
const landMask = new Uint8Array(SIZE);
for (let i = 0; i < SIZE; i++) landMask[i] = sea[i] ? 0 : 1;
const zeroDensity = new Float32Array(SIZE);
const zeroLanduse = new Int8Array(SIZE);
terrainProgress("natural-regions", "Terrain natural compartments", MAP_H + 5, MAP_H + 8);
const natural = buildNaturalCompartments(
landMask, sea, elevation, slope, river, ridgeField, valleyField, basinField, coastalLowland, flowAccum,
null, plain, agriculture, zeroDensity, zeroLanduse,
{ seed: seed + 17003, watershedId, targetCompartmentCount: clamp(Math.round((SIZE - sea.reduce((sum, value) => sum + value, 0)) / 45), 70, 360) }
{
seed: seed + 17003,
watershedId,
targetCompartmentCount: clamp(Math.round((SIZE - sea.reduce((sum, value) => sum + value, 0)) / 45), 70, 360),
progress: (label) => terrainProgress("natural-regions-detail", `Terrain ${label}`, MAP_H + 5, MAP_H + 8),
}
);
const sharedNaturalBarrierScore = natural.naturalBarrierScore || naturalBarrierScore;
const prefectureBorder = extractMaskBorder(prefectureMask, sea);
@ -2015,6 +2287,10 @@ export function generateTerrainAndRivers(seed, options = {}) {
terrainFrameScale,
terrainFrameOffsetX,
terrainFrameOffsetY,
requestedTerrainFrameOffsetX,
requestedTerrainFrameOffsetY,
worldFrameOffsetX,
worldFrameOffsetY,
};
return {

View file

@ -582,8 +582,18 @@ export function buildDensityFlowRoadTransportSystem(ctx) {
pairs.sort((a, b) => a.score - b.score);
let added = 0;
const virtualPenalty = new Float32Array(SIZE);
for (const pair of pairs) {
for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
const pair = pairs[pairIndex];
if (added >= maxRoutes) break;
onProgress?.({
status: "route-heartbeat",
key: `road:${mode}-flow-route`,
phase: "transport-routing",
workUnitId: `road-${mode}-flow-route-loop`,
label: `${mode} flow route ${pairIndex + 1}/${pairs.length}`,
completed: pairIndex,
total: pairs.length,
});
const path = routeBetweenTrafficCandidates(pair.A, pair.B, mode, baseCost, virtualPenalty, {
curvePenalty: mode === "expressway" ? 0.12 : 0.065,
penaltyStrength: mode === "expressway" ? 1.05 : 0.72,
@ -786,8 +796,18 @@ export function buildDensityFlowRoadTransportSystem(ctx) {
const maxAdded = options.maxAdded ?? (mode === "expressway" ? Math.min(7, Math.max(3, Math.ceil((options.nodeCount || 1) / 5))) : Math.min(26, Math.max(12, Math.ceil((options.nodeCount || 1) * 0.36))));
const maxExtra = options.maxExtra ?? config.backbone?.maxExtra ?? 0;
const maxDegree = options.maxDegree ?? config.backbone?.maxDegree ?? 3;
for (const pair of pairs) {
for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
const pair = pairs[pairIndex];
if (outPaths.length >= maxAdded) break;
onProgress?.({
status: "route-heartbeat",
key: `road:${mode}-backbone-route`,
phase: "transport-routing",
workUnitId: `road-${mode}-backbone-route-loop`,
label: `${mode} backbone route ${pairIndex + 1}/${pairs.length}`,
completed: pairIndex,
total: pairs.length,
});
const ak = keyOf(pair.A);
const bk = keyOf(pair.B);
const connects = find(ak) !== find(bk);

View file

@ -1,4 +1,40 @@
import { MAP_W, SIZE, clamp, indexOf, inside, walkGridPath } from "./mapUtils.js";
import { MAP_H, MAP_W, SIZE, clamp, indexOf, inside, walkGridPath } from "./mapUtils.js";
const radialInfluenceKernelCache = new Map();
// Exact cache for the integer-radius radial kernels used repeatedly by road,
// rail and expressway influence passes. Offset order is deliberately the same
// dy-major/dx-minor order as the former nested loops, and weights are Float64,
// so replacing repeated hypot/pow calls does not change field results.
export function getRadialInfluenceKernel(radius, exponent = 1.35) {
const r = Number(radius);
const e = Number(exponent);
if (!Number.isInteger(r) || r < 0 || !Number.isFinite(e)) return null;
const key = `${r}:${e}`;
let cached = radialInfluenceKernelCache.get(key);
if (cached) return cached;
const dx = [];
const dy = [];
const weight = [];
const denominator = Math.max(1, r);
for (let oy = -r; oy <= r; oy++) {
for (let ox = -r; ox <= r; ox++) {
if (ox * ox + oy * oy > r * r) continue;
const d = Math.hypot(ox, oy);
dx.push(ox);
dy.push(oy);
weight.push(Math.pow(1 - d / denominator, e));
}
}
cached = {
dx: Int16Array.from(dx),
dy: Int16Array.from(dy),
weight: Float64Array.from(weight),
length: dx.length,
};
radialInfluenceKernelCache.set(key, cached);
return cached;
}
export function pathSetSignature(paths) {
let cells = 0;
@ -47,6 +83,24 @@ export function pathAverageField(path, field) {
}
export function markPathInfluence(field, path, radius = 5, strength = 1, sea = null) {
const kernel = getRadialInfluenceKernel(radius, 1.35);
if (kernel) {
for (const [px, py] of path || []) {
for (let k = 0; k < kernel.length; k++) {
const x = px + kernel.dx[k];
const y = py + kernel.dy[k];
// The production grid is fixed MAP_W x MAP_H. Inline the bounds/index
// check in this hot loop while preserving the exact same accepted cells.
if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H) continue;
const i = y * MAP_W + x;
if (sea?.[i]) continue;
const v = strength * kernel.weight[k];
if (v > field[i]) field[i] = v;
}
}
return;
}
// Preserve legacy behavior for unusual non-integer radii.
for (const [px, py] of path || []) {
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {

View file

@ -0,0 +1,64 @@
import { generateMap } from "./mapPipeline.js";
import { collectTransferableBuffers } from "./transferUtils.js";
import { compactRawPatchCandidate, summarizeRawPatchCandidate } from "./rawPatchCandidate.js";
function scopeTaskProgress(event = {}, taskId) {
const scoped = { ...event };
if (event?.workUnitId != null && String(event.workUnitId).length > 0) {
scoped.workUnitId = `${taskId}/${String(event.workUnitId)}`;
}
return scoped;
}
if (typeof self !== "undefined") {
self.onmessage = (event) => {
const message = event.data || {};
if (message.type !== "generate-raw-patch-candidate") return;
const taskId = String(message.taskId || `raw-${message.id || 0}`);
const startedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
try {
let candidate = generateMap(Number(message.seed) >>> 0, {
...(message.mapOptions || {}),
// A boolean sentinel is sufficient: mapPipeline only records whether a
// boundary world exists. Actual seam/quality work remains in the parent
// patch worker after this raw candidate has been transferred back.
boundaryWorld: true,
onProgress: (progress) => {
self.postMessage({
type: "raw-patch-candidate-progress",
id: message.id,
taskId,
progress: scopeTaskProgress(progress, taskId),
});
},
});
const elapsedMs = (typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - startedAt;
// Transfer only the roots consumed by mapPatch. Full-map geography/debug
// graphs can exceed the actual patch payload and are never consulted by
// candidate merge/quality logic. Dropping them before structured clone
// creates a hard cross-worker memory bound for large tiled operations.
candidate = compactRawPatchCandidate(candidate);
const transferSummary = summarizeRawPatchCandidate(candidate);
const transferables = [...collectTransferableBuffers(candidate)];
self.postMessage({
type: "raw-patch-candidate-result",
id: message.id,
taskId,
ok: true,
elapsedMs,
transferSummary,
candidate,
}, transferables);
} catch (error) {
self.postMessage({
type: "raw-patch-candidate-result",
id: message.id,
taskId,
ok: false,
code: error?.code || "raw-patch-candidate-error",
error: error?.message || String(error),
stack: error?.stack || "",
});
}
};
}

81
src/rawPatchCandidate.js Normal file
View file

@ -0,0 +1,81 @@
// Raw production candidates are generated in nested workers for large patch
// selections. The complete generateMap() result contains large immutable
// geography/debug graphs that are useful to full-map callers but are never
// consumed by the patch merge pipeline. Structured-cloning those graphs for
// every tile multiplies peak memory and can trigger browser/OS memory pressure.
//
// Keep this schema deliberately conservative: every top-level typed array is
// retained (copyFullPipelineFields discovers cell fields dynamically), while
// only vector/metadata roots that mapPatch consumes are retained from ordinary
// arrays/objects. Primitive roots are cheap and kept for forward-compatible
// generation metadata.
export const RAW_PATCH_POINT_LAYER_KEYS = Object.freeze([
"villages", "geographicUrbanAnchors", "markets", "castles", "castleTowns", "castleRuins",
"ports", "crossings", "passes", "modernCities", "satelliteCities", "stations",
"interchanges", "industrialZones", "logisticsParks", "newTowns", "adminCenters",
"externalGateways", "prefectureRegions",
]);
export const RAW_PATCH_PATH_LAYER_KEYS = Object.freeze([
"premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads",
"railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways",
"icAccessRoads", "mainRivers", "tributaryRivers", "smallStreams", "riverPaths",
]);
const REQUIRED_ARRAY_ROOTS = new Set([
...RAW_PATCH_POINT_LAYER_KEYS,
...RAW_PATCH_PATH_LAYER_KEYS,
]);
const REQUIRED_OBJECT_ROOTS = new Set([
// validatePrecomputedPatchCandidate() verifies generationContext before the
// raw graph is accepted by the production patch generator.
"generationContext",
// Terrain identity is used by patch quality/continuity logic.
"terrainTemplate",
"terrainDebug",
// Only compartmentBorders is read today, but retaining the bounded admin
// diagnostic object avoids coupling this transport boundary to its internals.
"adminDebug",
// Preserve this if a future precompute path performs quality scoring before
// transfer. It is normally attached later by mapPatch.
"patchQuality",
]);
export function compactRawPatchCandidate(candidate) {
if (!candidate || typeof candidate !== "object") return candidate;
const compact = {};
for (const [key, value] of Object.entries(candidate)) {
if (value == null || typeof value !== "object") {
compact[key] = value;
continue;
}
if (ArrayBuffer.isView(value)) {
compact[key] = value;
continue;
}
if (Array.isArray(value)) {
if (REQUIRED_ARRAY_ROOTS.has(key)) compact[key] = value;
continue;
}
if (REQUIRED_OBJECT_ROOTS.has(key)) compact[key] = value;
}
return compact;
}
export function summarizeRawPatchCandidate(candidate) {
if (!candidate || typeof candidate !== "object") return { rootCount: 0, transferableBytes: 0 };
let transferableBytes = 0;
let typedArrayCount = 0;
for (const value of Object.values(candidate)) {
if (!ArrayBuffer.isView(value)) continue;
typedArrayCount += 1;
transferableBytes += Number(value.byteLength || 0);
}
return {
rootCount: Object.keys(candidate).length,
typedArrayCount,
transferableBytes,
};
}

View file

@ -10,9 +10,15 @@ const baseImageCache = new Map();
const urbanOverlayCache = new Map();
const prefectureFillCache = new Map();
const transportDebugHeatmapCache = new WeakMap();
const MAX_BASE_CACHE_IMAGES = 18;
const MAX_OVERLAY_CACHE_IMAGES = 18;
const MAX_SEGMENT_VECTOR_CACHE = 48;
// Keep only the currently useful raster frame. Previous camera/revision canvases
// can each be tens of MiB at low zoom and are cheaper to redraw than to retain
// while a full candidate world is also resident.
const MAX_BASE_CACHE_IMAGES = 1;
// Only the committed frame and current preview need raster overlays. Retaining
// 18 full-canvas generations made repeated Alternative previews consume
// hundreds of MiB at low zoom.
const MAX_OVERLAY_CACHE_IMAGES = 1;
const MAX_SEGMENT_VECTOR_CACHE = 16;
const CONTINUOUS_BASE_MODES = ["terrain", "development", "all"];
function fieldRefSignature(map) {
@ -1247,7 +1253,7 @@ function drawScaleBar(ctx, cellScreenSize = CELL_SIZE) {
ctx.restore();
}
export function drawMap(canvas, map, options) {
function* drawMapSteps(canvas, map, options) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
@ -1295,12 +1301,15 @@ export function drawMap(canvas, map, options) {
// 1. Base Terrain & Urban
drawBase(ctx, map, mode, continuousTerrain, terrainRenderScale);
markTiming("baseTerrain");
yield { phase: "baseTerrain" };
drawUrbanAreas(ctx, map, mode);
markTiming("urbanFill");
yield { phase: "urbanFill" };
const coastSegments = getCoastlineSegments(map);
drawVectorSegments(ctx, coastSegments, "rgba(120, 175, 210, 0.22)", 2.2, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
drawVectorSegments(ctx, coastSegments, "rgba(248, 250, 242, 0.68)", 1.1, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
markTiming("coastline");
yield { phase: "coastline" };
// 2. Rivers
const waterBlue = "rgba(116, 165, 202, 0.92)";
@ -1352,6 +1361,7 @@ export function drawMap(canvas, map, options) {
}, 1.0);
}
markTiming("rivers");
yield { phase: "rivers" };
const showTransportDebug = mode === "transport-debug";
const showModern = ["modern", "all", "development", "landuse", "borders-debug", "transport-debug"].includes(mode);
@ -1391,10 +1401,12 @@ export function drawMap(canvas, map, options) {
drawVectorSegments(ctx, finalPrefectureBorders, "rgba(110, 90, 110, 1)", 2.2, true, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
}
markTiming("adminBorders");
yield { phase: "adminBorders" };
if (!showFeatures) {
if (showSeamDiagnostics) drawSeamDiagnostics(ctx, map);
markTiming("seamDiagnostics");
yield { phase: "seamDiagnostics" };
return finish();
}
@ -1446,6 +1458,7 @@ export function drawMap(canvas, map, options) {
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(135, 160, 135, 0.88)", 2.4);
}
markTiming("transport");
yield { phase: "transport" };
// 6. Icons & Labels
if (["admin", "borders-debug"].includes(mode)) {
@ -1487,9 +1500,11 @@ export function drawMap(canvas, map, options) {
}
}
markTiming("icons");
yield { phase: "icons" };
if (showSeamDiagnostics) drawSeamDiagnostics(ctx, map);
markTiming("seamDiagnostics");
yield { phase: "seamDiagnostics" };
if (showLabels) {
const prefectureLabels = (map.prefectureRegions || []).map((p) => ({ ...p, isPrefectureLabel: true, forceLabel: true, labelPriorityBase: p.labelPriorityBase || 1900 }));
@ -1499,11 +1514,13 @@ export function drawMap(canvas, map, options) {
.map((p) => ({ ...p, labelStyle: "municipality", forceLabel: true, labelPriorityBase: 1200 }));
drawLabels(ctx, [...prefectureLabels, ...municipalLabels], Infinity);
markTiming("labels");
yield { phase: "labels" };
return finish();
}
if (mode === "borders-debug") {
drawLabels(ctx, prefectureLabels, Infinity);
markTiming("labels");
yield { phase: "labels" };
return finish();
}
const important = [
@ -1516,5 +1533,49 @@ export function drawMap(canvas, map, options) {
drawLabels(ctx, important, mode === "all" || mode === "history" ? 78 : 60);
}
markTiming("labels");
yield { phase: "labels" };
return finish();
}
export function drawMap(canvas, map, options) {
const steps = drawMapSteps(canvas, map, options);
let state = steps.next();
while (!state.done) state = steps.next();
return state.value;
}
export async function drawMapCooperative(canvas, map, options, cooperative = {}) {
const yieldControl = typeof cooperative.yieldControl === "function"
? cooperative.yieldControl
: () => new Promise((resolve) => setTimeout(resolve, 0));
const shouldCancel = typeof cooperative.shouldCancel === "function" ? cooperative.shouldCancel : () => false;
const steps = drawMapSteps(canvas, map, options);
let sliceStartedAt = nowMs();
let maxSliceMs = 0;
while (true) {
const state = steps.next();
const sliceMs = nowMs() - sliceStartedAt;
maxSliceMs = Math.max(maxSliceMs, sliceMs);
if (state.done) {
if (state.value && typeof state.value === "object") state.value.cooperativeMaxSliceMs = Math.round(maxSliceMs * 10) / 10;
return state.value;
}
if (shouldCancel()) {
try { steps.return?.(); } catch {}
const error = typeof DOMException === "function"
? new DOMException("Preview rendering cancelled.", "AbortError")
: Object.assign(new Error("Preview rendering cancelled."), { name: "AbortError" });
throw error;
}
await yieldControl(state.value?.phase || "render");
if (shouldCancel()) {
try { steps.return?.(); } catch {}
const error = typeof DOMException === "function"
? new DOMException("Preview rendering cancelled.", "AbortError")
: Object.assign(new Error("Preview rendering cancelled."), { name: "AbortError" });
throw error;
}
sliceStartedAt = nowMs();
}
}

View file

@ -1,8 +1,59 @@
import { MAP_H, MAP_W, SIZE } from "./mapUtils.js";
import { defaultCellFieldValue, isTypedCellField, worldFieldConstructor } from "./fieldSchema.js";
const DEFAULT_WORLD_PADDING_X = MAP_W;
const DEFAULT_WORLD_PADDING_Y = MAP_H;
// Keep enough off-screen room for immediate panning/expansion without paying
// for a nine-map-cell backing allocation before the first patch. Additional
// padding is still allocated on demand near camera edges.
const DEFAULT_WORLD_PADDING_X = Math.ceil(MAP_W * 0.5);
const DEFAULT_WORLD_PADDING_Y = Math.ceil(MAP_H * 0.5);
const MAX_WORLD_WIDTH = MAP_W * 6;
const MAX_WORLD_HEIGHT = MAP_H * 6;
const INITIAL_QUALITY_SETTLEMENT_KEYS = ["villages", "markets", "modernCities", "satelliteCities", "newTowns", "ports"];
const INITIAL_QUALITY_LABEL_KEYS = [...INITIAL_QUALITY_SETTLEMENT_KEYS, "adminCenters"];
const INITIAL_QUALITY_ROAD_KEYS = ["premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "expressways", "icAccessRoads"];
const INITIAL_QUALITY_RAIL_KEYS = ["railways", "branchRailways", "ringRailways"];
const FULL_MAP_ONLY_SOURCE_ROOTS = new Set([
// These graphs are required while generateMap() is building the initial map,
// but no renderer, viewport, patch, Apply or diagnostics path reads them once
// the padded world has been created. Keeping them in sourceMap makes every
// committed Worker mirror clone a large immutable geography graph.
"geography",
"naturalCompartments",
"geographicCompartmentProfiles",
"watershedProfiles",
"entitiesForNames",
"geographyDebug",
]);
function captureInitialQualityReference(initialMap) {
const sea = initialMap?.sea;
let landCells = 0;
if (sea && typeof sea.length === "number") {
for (let i = 0; i < sea.length; i++) if (!sea[i]) landCells++;
}
const count = (keys) => keys.reduce((sum, key) => sum + (Array.isArray(initialMap?.[key]) ? initialMap[key].length : 0), 0);
return {
landCells: Math.max(1, landCells),
settlements: count(INITIAL_QUALITY_SETTLEMENT_KEYS),
labels: count(INITIAL_QUALITY_LABEL_KEYS),
roadPathCount: count(INITIAL_QUALITY_ROAD_KEYS),
railPathCount: count(INITIAL_QUALITY_RAIL_KEYS),
};
}
function createSourceMetadata(initialMap) {
const sourceMap = { ...(initialMap || {}) };
// Raster cell fields already live in the padded world.fields arrays. Keeping
// the original 258x183 copies in sourceMap made every Worker mirror/input and
// accepted result carry a second obsolete raster set. Metadata, point layers,
// paths, diagnostics, and small lookup arrays remain intact.
for (const [name, value] of Object.entries(sourceMap)) {
if (isTypedCellField(value, SIZE) || FULL_MAP_ONLY_SOURCE_ROOTS.has(name)) delete sourceMap[name];
}
return sourceMap;
}
function makeWorldField(name, source, worldWidth, worldHeight, originX, originY) {
const Constructor = worldFieldConstructor(name, source.constructor);
@ -13,7 +64,7 @@ function makeWorldField(name, source, worldWidth, worldHeight, originX, originY)
for (let y = 0; y < MAP_H; y++) {
const srcRow = y * MAP_W;
const dstRow = (originY + y) * worldWidth + originX;
for (let x = 0; x < MAP_W; x++) out[dstRow + x] = source[srcRow + x];
out.set(source.subarray(srcRow, srcRow + MAP_W), dstRow);
}
return out;
}
@ -58,6 +109,10 @@ export function createWorldMap(initialMap, options = {}) {
fields.elevation.fill(0.08);
}
sanitizeInitialWorldFields(fields, worldWidth, worldHeight);
const generatedMask = new Uint8Array(worldWidth * worldHeight);
for (let y = 0; y < MAP_H; y++) {
generatedMask.fill(1, (originY + y) * worldWidth + originX, (originY + y) * worldWidth + originX + MAP_W);
}
return {
seed: initialMap?.seed ?? 0,
@ -70,10 +125,10 @@ export function createWorldMap(initialMap, options = {}) {
// Sea level is a world invariant. Patch candidates must classify water
// against this value instead of recalculating a local quantile.
seaLevel: Number.isFinite(initialMap?.seaLevel) ? initialMap.seaLevel : 0.30,
sourceMap: initialMap,
sourceMap: createSourceMetadata(initialMap),
initialQualityReference: captureInitialQualityReference(initialMap),
fields,
invalidatedRects: [],
humanPatchHistory: [],
generatedMask,
generatedRects: [{
x0: originX,
y0: originY,
@ -104,7 +159,12 @@ export function clampCameraToWorld(camera, world, viewWidth = MAP_W, viewHeight
function expandRectByOffset(rect, dx, dy) {
if (!rect) return rect;
return { ...rect, x0: rect.x0 + dx, y0: rect.y0 + dy, x1: rect.x1 + dx, y1: rect.y1 + dy };
const out = { ...rect };
if (Number.isFinite(rect.x0)) out.x0 = rect.x0 + dx;
if (Number.isFinite(rect.y0)) out.y0 = rect.y0 + dy;
if (Number.isFinite(rect.x1)) out.x1 = rect.x1 + dx;
if (Number.isFinite(rect.y1)) out.y1 = rect.y1 + dy;
return out;
}
function shiftSelectionShape(shape, dx, dy) {
@ -140,12 +200,26 @@ function shiftPatchMetadata(item, dx, dy) {
}
if (out?.selectionShape) out.selectionShape = shiftSelectionShape(out.selectionShape, dx, dy);
if (out?.generatedFootprint) out.generatedFootprint = shiftGeneratedFootprint(out.generatedFootprint, dx, dy);
if (out?.rects) out.rects = shiftPatchMetadata(out.rects, dx, dy);
if (out?.validation?.rect) out.validation = { ...out.validation, rect: shiftPatchMetadata(out.validation.rect, dx, dy) };
if (out?.seamDiagnostics) {
const seam = { ...out.seamDiagnostics };
for (const key of ["issuePoints", "markerPoints"]) {
if (Array.isArray(seam[key])) seam[key] = seam[key].map((point) => ({ ...point, x: point.x + dx, y: point.y + dy }));
}
for (const key of ["outlineSegments", "seamSegments"]) {
if (Array.isArray(seam[key])) seam[key] = seam[key].map((segment) => Array.isArray(segment)
? segment.map((point) => Array.isArray(point) ? [point[0] + dx, point[1] + dy] : point)
: segment);
}
out.seamDiagnostics = seam;
}
return out;
}
function shiftRectCollections(world, dx, dy) {
if (!dx && !dy) return;
for (const key of ["generatedRects", "invalidatedRects", "humanPatchHistory"]) {
for (const key of ["generatedRects"]) {
if (!Array.isArray(world[key])) continue;
world[key] = world[key].map((item) => shiftPatchMetadata(item, dx, dy));
}
@ -154,10 +228,20 @@ function shiftRectCollections(world, dx, dy) {
function expandWorldMap(world, margins = {}) {
if (!world) return { world, dx: 0, dy: 0, expanded: false };
const left = Math.max(0, Math.floor(margins.left || 0));
const right = Math.max(0, Math.floor(margins.right || 0));
const top = Math.max(0, Math.floor(margins.top || 0));
const bottom = Math.max(0, Math.floor(margins.bottom || 0));
let left = Math.max(0, Math.floor(margins.left || 0));
let right = Math.max(0, Math.floor(margins.right || 0));
let top = Math.max(0, Math.floor(margins.top || 0));
let bottom = Math.max(0, Math.floor(margins.bottom || 0));
const remainingWidth = Math.max(0, MAX_WORLD_WIDTH - world.width);
const remainingHeight = Math.max(0, MAX_WORLD_HEIGHT - world.height);
if (left + right > remainingWidth) {
left = Math.min(left, remainingWidth);
right = Math.min(right, remainingWidth - left);
}
if (top + bottom > remainingHeight) {
top = Math.min(top, remainingHeight);
bottom = Math.min(bottom, remainingHeight - top);
}
if (!left && !right && !top && !bottom) return { world, dx: 0, dy: 0, expanded: false };
const oldWidth = world.width;
const oldHeight = world.height;
@ -173,10 +257,19 @@ function expandWorldMap(world, margins = {}) {
for (let y = 0; y < oldHeight; y++) {
const srcRow = y * oldWidth;
const dstRow = (y + top) * newWidth + left;
for (let x = 0; x < oldWidth; x++) out[dstRow + x] = field[srcRow + x];
out.set(field.subarray(srcRow, srcRow + oldWidth), dstRow);
}
newFields[name] = out;
}
if (ArrayBuffer.isView(world.generatedMask)) {
const generatedMask = new Uint8Array(newWidth * newHeight);
for (let y = 0; y < oldHeight; y++) {
const srcStart = y * oldWidth;
const dstStart = (y + top) * newWidth + left;
generatedMask.set(world.generatedMask.subarray(srcStart, srcStart + oldWidth), dstStart);
}
world.generatedMask = generatedMask;
}
world.width = newWidth;
world.height = newHeight;
world.originX += left;

View file

@ -1,4 +1,4 @@
import { MAP_H, MAP_W, SIZE, worldIndexOf } from "./mapUtils.js";
import { MAP_H, MAP_W, worldIndexOf } from "./mapUtils.js";
import { defaultCellFieldValue } from "./fieldSchema.js";
const EMPTY_ARRAY_KEYS = new Set([
@ -26,6 +26,43 @@ const POINT_ARRAY_KEYS = new Set([
"externalGateways", "prefectureRegions",
]);
// Renderer, hover/stat, and transport-debug consumers only read this raster
// subset. Copying every simulation/debug field into a viewport on every
// preview, Apply, pan, and zoom caused avoidable typed-array allocation and GC.
const VIEWPORT_RASTER_FIELDS = new Set([
"adminId", "municipalityId", "agriculture", "coastalLowland", "elevation", "flowAccum",
"focusedPrefectureMask", "humanRegionMask", "landuse", "naturalBarrierScore", "plain",
"populationDensity", "prefectureMask", "prefectureRegionId", "railInfluence2", "regionId",
"ridgeField", "river", "roadInfluence", "sea", "settlementCluster", "settlementScore",
"slope", "stationInfluence", "surfaceTextureField", "valleyField", "visibleRavineField",
]);
const viewportCache = new WeakMap();
function viewportCacheKey(world, camera, viewWidth, viewHeight, light) {
return [
Math.round(camera?.x || 0), Math.round(camera?.y || 0), viewWidth, viewHeight,
light ? 1 : 0, world?.width || 0, world?.height || 0,
world?.originX || 0, world?.originY || 0,
world?.renderRevision || 0, world?.patchGenerationSerial || 0,
].join(":");
}
function rememberViewport(world, key, viewport) {
if (!world || typeof world !== "object") return viewport;
let entries = viewportCache.get(world);
if (!entries) {
entries = new Map();
viewportCache.set(world, entries);
}
entries.set(key, viewport);
// A viewport owns one typed array per rendered raster field. Retaining four
// camera/revision snapshots multiplied memory during preview generation; the
// current view is the only one required for correctness.
while (entries.size > 1) entries.delete(entries.keys().next().value);
return viewport;
}
function copyViewportField(name, source, world, camera, viewWidth, viewHeight) {
const Constructor = source.constructor;
const out = new Constructor(viewWidth * viewHeight);
@ -111,48 +148,17 @@ function transformSegments(segments, camera, originX, originY, viewWidth = MAP_W
}
function copySourceMapViewportField(source, camera, originX, originY, viewWidth, viewHeight) {
if (!ArrayBuffer.isView(source) || typeof source.length !== "number" || source.length !== SIZE) return source;
const out = new source.constructor(viewWidth * viewHeight);
const cx = Math.round(camera.x || 0);
const cy = Math.round(camera.y || 0);
for (let y = 0; y < viewHeight; y++) {
for (let x = 0; x < viewWidth; x++) {
const sx = cx + x - originX;
const sy = cy + y - originY;
if (sx >= 0 && sy >= 0 && sx < MAP_W && sy < MAP_H) out[y * viewWidth + x] = source[sy * MAP_W + sx];
}
}
return out;
}
function transformTransportDebug(debug, camera, originX, originY, viewport = null, viewWidth = MAP_W, viewHeight = MAP_H) {
function transformTransportDebug(debug, camera, originX, originY, viewWidth = MAP_W, viewHeight = MAP_H) {
if (!debug?.layers) return debug;
const layers = { ...debug.layers };
// Potential rasters are never read from this transformed debug object.
// renderer.js deterministically derives the four visible heatmaps from the
// current viewport fields. Copying every fixed-map potential raster and then
// synthesizing four Float32 viewport rasters here allocated both versions only
// for them to be ignored. Keep vector/point diagnostics and let the renderer
// create its single Uint8 heatmap set.
for (const [key, value] of Object.entries(layers)) {
if (ArrayBuffer.isView(value) && value.length === SIZE) layers[key] = copySourceMapViewportField(value, camera, originX, originY, viewWidth, viewHeight);
}
// The original transport-debug potential layers are fixed-map arrays. Once the
// viewport pans into patched world cells, synthesize equivalent viewport-sized
// debug fields from the current world-backed fields so the color overlay moves
// with the terrain instead of staying tied to the initial source map.
if (viewport) {
const n = viewWidth * viewHeight;
const make = (fn) => {
const out = new Float32Array(n);
for (let i = 0; i < n; i++) out[i] = fn(i);
return out;
};
const sea = viewport.sea || new Uint8Array(n);
const slope = viewport.slope || new Float32Array(n);
const plain = viewport.plain || new Float32Array(n);
const pop = viewport.populationDensity || viewport.settlementScore || new Float32Array(n);
const road = viewport.roadInfluence || new Float32Array(n);
const rail = viewport.railInfluence2 || viewport.stationInfluence || new Float32Array(n);
layers.expresswayPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, road[i] * 0.72 + pop[i] * 0.42 + plain[i] * 0.22 - slope[i] * 0.52)));
layers.nationalRoadPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, road[i] * 0.58 + pop[i] * 0.55 + plain[i] * 0.18 - slope[i] * 0.38)));
layers.railPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, rail[i] * 0.72 + pop[i] * 0.38 + plain[i] * 0.26 - slope[i] * 0.72)));
layers.slopeSeaPenalty = make((i) => sea[i] ? 1 : Math.max(0, Math.min(1, slope[i] * 1.35)));
if (ArrayBuffer.isView(value)) delete layers[key];
}
if (Array.isArray(layers.components)) {
layers.components = layers.components.map((component) => ({
@ -191,9 +197,13 @@ export function getViewportMap(world, camera, viewWidth = MAP_W, viewHeight = MA
x: Math.round(camera?.x || 0),
y: Math.round(camera?.y || 0),
};
const cacheKey = viewportCacheKey(world, normalizedCamera, viewWidth, viewHeight, !!options.light);
const cached = viewportCache.get(world)?.get(cacheKey);
if (cached) return cached;
const viewport = buildEmptyViewportFromSource(sourceMap, world, normalizedCamera, viewWidth, viewHeight);
for (const [name, value] of Object.entries(world?.fields || {})) {
if (options.includeAllFields !== true && !VIEWPORT_RASTER_FIELDS.has(name)) continue;
viewport[name] = copyViewportField(name, value, world, normalizedCamera, viewWidth, viewHeight);
}
@ -204,7 +214,7 @@ export function getViewportMap(world, camera, viewWidth = MAP_W, viewHeight = MA
viewport.adminDebug = null;
viewport.transportDebug = null;
viewport.patchSeamDiagnostics = null;
return viewport;
return rememberViewport(world, cacheKey, viewport);
}
const originX = world?.originX || 0;
@ -223,7 +233,7 @@ export function getViewportMap(world, camera, viewWidth = MAP_W, viewHeight = MA
lowlandAdminSeeds: transformPointArray(sourceMap.adminDebug.lowlandAdminSeeds || [], normalizedCamera, originX, originY, 36, false, viewWidth, viewHeight),
};
}
if (sourceMap.transportDebug) viewport.transportDebug = transformTransportDebug(sourceMap.transportDebug, normalizedCamera, originX, originY, viewport, viewWidth, viewHeight);
if (sourceMap.transportDebug) viewport.transportDebug = transformTransportDebug(sourceMap.transportDebug, normalizedCamera, originX, originY, viewWidth, viewHeight);
if (sourceMap.patchSeamDiagnostics) {
viewport.patchSeamDiagnostics = {
...sourceMap.patchSeamDiagnostics,
@ -240,5 +250,5 @@ export function getViewportMap(world, camera, viewWidth = MAP_W, viewHeight = MA
};
}
return viewport;
return rememberViewport(world, cacheKey, viewport);
}

View file

@ -0,0 +1,81 @@
import { Worker } from "node:worker_threads";
import { performance } from "node:perf_hooks";
import { generateMap } from "../src/mapPipeline.js";
import { createWorldMap } from "../src/worldMap.js";
const worldSeed = 8;
const initial = generateMap(worldSeed);
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) {
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);
const startedAt = performance.now();
const seed = 123;
worker.on("error", reject);
worker.on("message", async (message) => {
if (message.id !== id || message.type === "progress") return;
clearTimeout(timer);
try { await worker.terminate(); } catch {}
resolve({ message, elapsedMs: Math.round(performance.now() - startedAt) });
});
worker.postMessage({
id,
world: structuredClone(baseline),
rect,
options: {
patchMode,
terrainType,
variant: 0,
seed,
maxQualityRetries: 0,
qualityTerrainAttempts: 1,
acceptBestAvailableQuality: false,
includeSeamVisualization: false,
},
search: {
searchId: `coverage-${patchMode}`,
operationId: `coverage-${patchMode}`,
committedRevision: 1,
workerEpoch: 1,
executionAttempt: 1,
totalCandidateCount: 1,
candidatePlan: [{ candidateId: `coverage-${patchMode}:0`, candidateOrdinal: 1, variant: 0, seed }],
},
});
});
}
for (const [index, patchMode] of ["auto", "expansion"].entries()) {
const { message, elapsedMs } = await runWorkerCase(patchMode, index + 1);
const result = message.result || {};
const ok = message.ok === true
&& result.ok === true
&& result.searchStatus === "succeeded"
&& result.patchMode === "expansion"
&& result.tiledExpansion === true
&& Number(result.tileCount) === 1
&& Number(result.candidateUnmappedActiveCells || 0) === 0
&& result.seamDiagnostics?.hardPass === true;
console.log(JSON.stringify({
patchMode,
ok,
elapsedMs,
workerOk: message.ok === true,
searchStatus: result.searchStatus || null,
resultCode: result.code || null,
resolvedPatchMode: result.patchMode || null,
tiledExpansion: result.tiledExpansion === true,
tileCount: Number(result.tileCount || 0),
candidateUnmappedActiveCells: Number(result.candidateUnmappedActiveCells || 0),
seamPass: result.seamDiagnostics?.hardPass === true,
reason: result.reason || message.error || null,
}, null, 2));
if (!ok) process.exitCode = 1;
}

View file

@ -0,0 +1,19 @@
<!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>

View file

@ -0,0 +1,231 @@
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);
}

View file

@ -0,0 +1,134 @@
import { Worker } from "node:worker_threads";
import { performance } from "node:perf_hooks";
import { generateMap } from "../src/mapPipeline.js";
import { createWorldMap } from "../src/worldMap.js";
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 terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
const seed = deriveSeed(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) {
clearInterval(memoryTimer);
clearTimeout(timeoutTimer);
try { await worker.terminate(); } catch {}
const elapsedMs = Math.round(performance.now() - startedAt);
const finalMerge = payload?.candidateQuality?.finalMerge || null;
const qualityPass = payload?.candidateQuality?.hardPass === true && finalMerge?.hardPass === true;
const seamPass = payload?.seamDiagnostics?.hardPass === true;
const withinBudget = elapsedMs < budgetMs;
const ok = payload?.ok === true && payload?.searchStatus === "succeeded" && qualityPass && seamPass && withinBudget;
console.log(JSON.stringify({
ok,
worldSeed,
candidateVariant,
workerResultOk: payload?.ok === true,
initialGenerationMs,
elapsedMs,
budgetMs,
withinBudget,
maxRssBytes,
maxHeapBytes,
progressEvents,
lastLabel,
searchStatus: payload?.searchStatus || null,
terminalCode: payload?.terminalCode || payload?.code || null,
qualityPass,
seamPass,
finalMerge,
humanTerrainReconciliation: payload?.seamDiagnostics?.humanTerrainReconciliation || null,
error: payload?.error || null,
reason: payload?.reason || null,
}, null, 2));
process.exit(ok ? 0 : exitCode || 1);
}
const timeoutTimer = setTimeout(() => finish(2, { ok: false, error: `Timed out after ${timeoutMs} ms.` }), timeoutMs);
worker.on("message", (message) => {
if (message.id !== 1) return;
if (message.type === "progress") {
progressEvents++;
const progress = message.progress || {};
const label = String(progress.label || "");
if (label && label !== lastLabel && (/Large expansion tile|finalization complete/i.test(label))) {
lastLabel = label;
console.error(`[max-worker] ${Math.round(performance.now() - startedAt)} ms: ${label}`);
}
return;
}
finish(message.ok ? 1 : 3, {
ok: message.ok,
code: message.code,
error: message.error,
searchStatus: message.result?.searchStatus,
terminalCode: message.result?.code,
reason: message.result?.reason,
candidateQuality: message.result?.candidateQuality,
seamDiagnostics: message.result?.seamDiagnostics,
});
});
worker.on("error", (error) => finish(3, { ok: false, error: error?.stack || String(error) }));
worker.postMessage({
id: 1,
world,
rect,
options: {
patchMode: "expansion",
terrainType,
variant: candidateVariant,
seed,
maxQualityRetries: 0,
qualityTerrainAttempts: 1,
acceptBestAvailableQuality: false,
includeSeamVisualization: false,
},
search: {
searchId: "max-worker-benchmark",
operationId: "max-worker-benchmark",
committedRevision: 1,
workerEpoch: 1,
executionAttempt: 1,
totalCandidateCount: 1,
candidatePlan,
},
});

View file

@ -0,0 +1,942 @@
import { MAP_H, MAP_W, fbm, valueNoise } from "../src/mapUtils.js";
import { createExactNoiseMemo } from "../src/mapTerrain.js";
import {
capturePatchTransactionSnapshot,
buildLargeExpansionTiles,
buildPatchRects,
buildRawPatchCandidateRequest,
buildTiledFinalQualityBasis,
captureStrictMetadataSnapshot,
captureStrictSelectionFieldSnapshot,
normalizePatchPrefectureCapitals,
preparePatchTransactionFields,
reconcileGeneratedHumanPointsWithFinalTerrain,
restorePatchTransactionSnapshot,
refreshPatchPrefectureMetadata,
synchronizePatchAdministrativeMetadata,
synchronizePatchMunicipalityField,
} from "../src/mapPatch.js";
import {
applyCommittedMirrorDelta,
buildCommittedMirrorDelta,
buildCommittedMirrorDeltaFromTransaction,
buildMainThreadTransferDelta,
hashCommittedWorld,
runPatchCandidateSearch,
runPatchCandidateSearchAsync,
scheduleRawCandidateSequence,
shutdownRawCandidateWorkers,
} from "../src/mapPatchWorker.js";
import { hashCommittedWorldAsync, materializeCommittedWorldDelta, materializeCommittedWorldDeltaCooperative } from "../src/committedWorldDelta.js";
import { getViewportMap } from "../src/worldViewport.js";
import { createWorldMap } from "../src/worldMap.js";
import { compactRawPatchCandidate, summarizeRawPatchCandidate } from "../src/rawPatchCandidate.js";
function assert(condition, message) {
if (!condition) throw new Error(message);
console.log(`OK: ${message}`);
}
function testPointInPolygon(px, py, polygon) {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x + 0.5;
const yi = polygon[i].y + 0.5;
const xj = polygon[j].x + 0.5;
const yj = polygon[j].y + 0.5;
const denomRaw = yj - yi;
const denom = Math.abs(denomRaw) < 1e-6 ? (denomRaw < 0 ? -1e-6 : 1e-6) : denomRaw;
if (((yi > py) !== (yj > py)) && px < ((xj - xi) * (py - yi)) / denom + xi) inside = !inside;
}
return inside;
}
const base = { fields: { marker: new Uint8Array([1]) } };
const calls = [];
const progress = [];
const search = runPatchCandidateSearch({
id: 1,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: {
searchId: "unit-search",
committedRevision: 1,
workerEpoch: 1,
candidatePlan: [{ variant: 5, seed: 105 }, { variant: 6, seed: 106 }],
},
}, {
cloneWorld: structuredClone,
onProgress: (message) => progress.push(message.progress),
generateCandidate: (world, rect, options) => {
calls.push({ variant: options.variant, baseline: world.fields.marker[0] });
world.fields.marker[0] = options.variant;
return options.variant === 5
? { ok: false, code: "patch-quality-gate-failed", reason: "forced reject" }
: { ok: true, variant: options.variant, seed: options.seed, seamDiagnostics: { hardPass: true } };
},
});
assert(search.result?.ok && search.result.actualVariant === 6, "content rejection advances to the next complete candidate");
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 repeatedPipelineProgress = [];
const repeatedPipelineSearch = runPatchCandidateSearch({
id: 101,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: {
searchId: "unit-repeated-pipeline-progress",
candidatePlan: [{ variant: 11, seed: 111 }, { variant: 12, seed: 112 }],
},
}, {
cloneWorld: structuredClone,
onProgress: (message) => repeatedPipelineProgress.push(message.progress),
generateCandidate: (world, rect, options) => {
// Every complete candidate legitimately reuses the same producer-local
// work-unit name. The search controller must scope it by candidate rather
// than treating candidate 2 as a reset of candidate 1.
options.onProgress({ phase: "terrain", key: "terrain:base", workUnitId: "terrain-production", completed: 0, total: 10 });
options.onProgress({ phase: "terrain", key: "terrain:base", workUnitId: "terrain-production", completed: 10, total: 10 });
return options.variant === 11
? { ok: false, code: "patch-quality-gate-failed", reason: "forced first-candidate reject" }
: { ok: true, seamDiagnostics: { hardPass: true } };
},
});
const repeatedTerrainUnits = repeatedPipelineProgress
.filter((event) => event.key === "terrain:base")
.map((event) => event.workUnitId);
assert(repeatedPipelineSearch.ok && repeatedPipelineSearch.result?.actualVariant === 12
&& repeatedTerrainUnits.some((id) => id === "candidate-1/terrain-production")
&& repeatedTerrainUnits.some((id) => id === "candidate-2/terrain-production"),
"rejected candidates may restart the same producer-local bounded work under a candidate-scoped workUnitId");
const sequenceForwardSentinel = () => ({ promises: [], done: Promise.resolve(), release() {}, cancel() {} });
let forwardedSequence = null;
const asyncForwardSearch = await runPatchCandidateSearchAsync({
id: 102,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: { searchId: "unit-sequence-forward", candidatePlan: [{ variant: 1, seed: 1 }] },
}, {
cloneWorld: structuredClone,
precomputeRawCandidateSequence: sequenceForwardSentinel,
generateCandidate: async (world, rect, options) => {
forwardedSequence = options._precomputeRawCandidateSequence;
return { ok: true, seamDiagnostics: { hardPass: true } };
},
});
assert(asyncForwardSearch.ok && asyncForwardSearch.result?.ok && forwardedSequence === sequenceForwardSentinel,
"async candidate search forwards the continuous raw-tile sequence provider into production generation");
const invalidProgress = runPatchCandidateSearch({
id: 2,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: { searchId: "unit-invalid-progress", candidatePlan: [{ variant: 1, seed: 1 }] },
}, {
cloneWorld: structuredClone,
generateCandidate: (world, rect, options) => {
options.onProgress({ key: "runaway", completed: 4, total: 3 });
return { ok: true, seamDiagnostics: { hardPass: true } };
},
});
assert(!invalidProgress.ok && invalidProgress.code === "worker-progress-invariant", "runaway bounded work is an invariant failure");
const distinctRouteUnits = runPatchCandidateSearch({
id: 3,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: { searchId: "unit-distinct-route-progress", candidatePlan: [{ variant: 1, seed: 1 }] },
}, {
cloneWorld: structuredClone,
generateCandidate: (world, rect, options) => {
options.onProgress({ phase: "transport-routing", key: "route:corridor", workUnitId: "corridor-a", completed: 2048, total: 8800 });
options.onProgress({ phase: "transport-routing", key: "route:corridor", workUnitId: "corridor-b", completed: 2048, total: 6094 });
options.onProgress({ phase: "transport-routing-detail", key: "route:corridor", workUnitId: "corridor-b", completed: 4096, total: 6094 });
return { ok: true, seamDiagnostics: { hardPass: true } };
},
});
assert(distinctRouteUnits.ok && distinctRouteUnits.result?.ok, "independent route work units may share a display key and use different finite totals");
const changedRouteTotal = runPatchCandidateSearch({
id: 4,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: { searchId: "unit-changed-route-total", candidatePlan: [{ variant: 1, seed: 1 }] },
}, {
cloneWorld: structuredClone,
generateCandidate: (world, rect, options) => {
options.onProgress({ phase: "transport-routing", key: "route:corridor", workUnitId: "corridor-one", completed: 1024, total: 8800 });
options.onProgress({ phase: "transport-routing-detail", key: "route:corridor", workUnitId: "corridor-one", completed: 2048, total: 6094 });
return { ok: true, seamDiagnostics: { hardPass: true } };
},
});
assert(!changedRouteTotal.ok && changedRouteTotal.code === "worker-progress-invariant", "one finite workUnitId cannot change its total across phase changes");
const missingRouteWorkUnit = runPatchCandidateSearch({
id: 5,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: { searchId: "unit-missing-route-work-unit", candidatePlan: [{ variant: 1, seed: 1 }] },
}, {
cloneWorld: structuredClone,
generateCandidate: (world, rect, options) => {
options.onProgress({ phase: "transport-routing", key: "route:corridor", completed: 2048, total: 8800 });
return { ok: true, seamDiagnostics: { hardPass: true } };
},
});
assert(!missingRouteWorkUnit.ok && missingRouteWorkUnit.code === "worker-progress-invariant", "bounded progress requires an explicit machine workUnitId");
const recoveredCandidateProgress = [];
const recoveredCandidateSearch = runPatchCandidateSearch({
id: 6,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: {
searchId: "unit-recovered-candidate-progress",
totalCandidateCount: 3,
executionAttempt: 2,
candidatePlan: [
{ candidateId: "candidate-2", candidateOrdinal: 2, variant: 2, seed: 2 },
{ candidateId: "candidate-3", candidateOrdinal: 3, variant: 3, seed: 3 },
],
},
}, {
cloneWorld: structuredClone,
onProgress: (message) => recoveredCandidateProgress.push(message.progress),
generateCandidate: (world, rect, options) => options.variant === 2
? { ok: false, code: "patch-quality-gate-failed", reason: "forced post-restart reject" }
: { ok: true, seamDiagnostics: { hardPass: true } },
});
const recoveredSearchTicks = recoveredCandidateProgress
.filter((event) => event.workUnitId === "candidate-search" && Number.isFinite(event.completed))
.map((event) => Number(event.completed));
assert(recoveredCandidateSearch.ok && recoveredCandidateSearch.result?.actualVariant === 3
&& recoveredSearchTicks.every((value, index) => index === 0 || value >= recoveredSearchTicks[index - 1]),
"Worker recovery may resume at candidate ordinal 2 without moving candidate-search progress backward");
const recoveredAsyncCandidateProgress = [];
const recoveredAsyncCandidateSearch = await runPatchCandidateSearchAsync({
id: 7,
world: base,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: {
searchId: "unit-recovered-async-candidate-progress",
totalCandidateCount: 3,
executionAttempt: 2,
candidatePlan: [
{ candidateId: "candidate-2", candidateOrdinal: 2, variant: 2, seed: 2 },
{ candidateId: "candidate-3", candidateOrdinal: 3, variant: 3, seed: 3 },
],
},
}, {
cloneWorld: structuredClone,
onProgress: (message) => recoveredAsyncCandidateProgress.push(message.progress),
generateCandidate: async (world, rect, options) => options.variant === 2
? { ok: false, code: "patch-quality-gate-failed", reason: "forced post-restart reject" }
: { ok: true, seamDiagnostics: { hardPass: true } },
});
const recoveredAsyncTicks = recoveredAsyncCandidateProgress
.filter((event) => event.workUnitId === "candidate-search" && Number.isFinite(event.completed))
.map((event) => Number(event.completed));
assert(recoveredAsyncCandidateSearch.ok && recoveredAsyncCandidateSearch.result?.actualVariant === 3
&& recoveredAsyncTicks.every((value, index) => index === 0 || value >= recoveredAsyncTicks[index - 1]),
"async production recovery preserves global candidate ordinals in bounded search progress");
const asyncHashFixture = {
width: 3, height: 2, originX: -4, originY: 7,
fields: {
a: new Float32Array([0, 1.25, -2.5, 3.75, 0, 9]),
b: new Int16Array([7, -8, 9, -10, 11, -12]),
},
generatedMask: new Uint8Array([0, 1, 1, 0, 1, 0]),
sourceMap: { points: [{ x: 1, y: 2, name: "甲" }], tags: new Set(["b", "a"]) },
serial: 4,
};
let asyncHashYields = 0;
const exactAsyncHash = await hashCommittedWorldAsync(asyncHashFixture, {
yieldEvery: 7,
yieldControl: async () => { asyncHashYields++; },
});
assert(exactAsyncHash === hashCommittedWorld(asyncHashFixture) && asyncHashYields > 0,
"cooperative committed-world hash is bit-identical to the synchronous Worker hash while yielding bounded chunks");
let abortHash = false;
try {
let abortAfterYield = false;
await hashCommittedWorldAsync(asyncHashFixture, {
yieldEvery: 4,
yieldControl: async () => { abortAfterYield = true; },
shouldAbort: () => abortAfterYield,
});
} catch (error) {
abortHash = error?.name === "AbortError";
}
assert(abortHash, "cooperative committed-world hash observes cancellation between chunks");
const deltaBase = {
width: 4, height: 2,
fields: {
field: new Int32Array([1, 2, 3, 4, 5, 6, 7, 8]),
stable: new Uint8Array([9, 9, 9, 9, 9, 9, 9, 9]),
},
generatedMask: new Uint8Array(8), sourceMap: { points: [{ x: 1 }] }, serial: 1,
};
const deltaTarget = structuredClone(deltaBase);
deltaBase.sourceMap.unchangedPaths = [[[0, 0], [1, 1]]];
deltaTarget.sourceMap.unchangedPaths = [[[0, 0], [1, 1]]];
deltaTarget.fields.field[2] = 30;
deltaTarget.fields.field[7] = 80;
deltaTarget.generatedMask[6] = 1;
deltaTarget.sourceMap.points.push({ x: 2 });
deltaTarget.serial = 2;
const committedDelta = buildCommittedMirrorDelta(deltaBase, deltaTarget);
const deltaApplied = applyCommittedMirrorDelta(structuredClone(deltaBase), committedDelta);
assert(JSON.stringify(Array.from(deltaApplied.fields.field)) === JSON.stringify(Array.from(deltaTarget.fields.field))
&& JSON.stringify(Array.from(deltaApplied.generatedMask)) === JSON.stringify(Array.from(deltaTarget.generatedMask))
&& JSON.stringify(deltaApplied.sourceMap) === JSON.stringify(deltaTarget.sourceMap)
&& deltaApplied.serial === 2, "transactional Apply delta exactly reproduces the accepted world");
assert(!("sourceMap" in committedDelta)
&& !("unchangedPaths" in (committedDelta.sourceMapDelta?.set || {}))
&& !("points" in (committedDelta.sourceMapDelta?.set || {}))
&& committedDelta.sourceMapDelta?.arraySplices?.points?.start === 1
&& committedDelta.sourceMapDelta?.arraySplices?.points?.deleteCount === 0
&& committedDelta.sourceMapDelta?.arraySplices?.points?.items?.length === 1
&& !("width" in (committedDelta.metaDelta?.set || {})), "Apply delta retains only changed source and metadata keys");
const materializeBaseHash = hashCommittedWorld(deltaBase);
const materializedDelta = buildCommittedMirrorDelta(deltaBase, deltaTarget);
const materializedPreview = materializeCommittedWorldDelta(deltaBase, materializedDelta, { consumeMetadata: true });
assert(hashCommittedWorld(materializedPreview) === hashCommittedWorld(deltaTarget)
&& hashCommittedWorld(deltaBase) === materializeBaseHash
&& materializedPreview.fields.field !== deltaBase.fields.field
&& materializedPreview.fields.stable === deltaBase.fields.stable
&& materializedPreview.sourceMap.points !== deltaBase.sourceMap.points
&& materializedPreview.sourceMap.unchangedPaths === deltaBase.sourceMap.unchangedPaths,
"preview materialization clones changed fields/layers only and leaves the committed world immutable");
let cooperativeYields = 0;
const cooperativePreview = await materializeCommittedWorldDeltaCooperative(deltaBase, buildCommittedMirrorDelta(deltaBase, deltaTarget), {
consumeMetadata: true,
chunkBytes: 8,
yieldControl: async () => { cooperativeYields++; },
});
assert(hashCommittedWorld(cooperativePreview) === hashCommittedWorld(deltaTarget)
&& hashCommittedWorld(deltaBase) === materializeBaseHash
&& cooperativeYields > 0,
"cooperative preview materialization is exact and yields between bounded raster chunks");
const cancellationBase = {
width: 4096, height: 1,
fields: { field: new Uint8Array(4096) },
generatedMask: new Uint8Array(4096),
sourceMap: {},
};
const cancellationTarget = structuredClone(cancellationBase);
cancellationTarget.fields.field.fill(7);
let cancellationYields = 0;
let materializationCancelled = false;
try {
await materializeCommittedWorldDeltaCooperative(cancellationBase, buildCommittedMirrorDelta(cancellationBase, cancellationTarget), {
consumeMetadata: true,
chunkBytes: 64,
yieldControl: async () => { cancellationYields++; },
shouldCancel: () => cancellationYields >= 2,
});
} catch (error) {
materializationCancelled = error?.name === "AbortError";
}
assert(materializationCancelled && cancellationYields >= 2 && cancellationBase.fields.field[0] === 0,
"cooperative preview materialization observes cancellation without mutating the committed world");
const transferDelta = buildMainThreadTransferDelta(materializedDelta);
const transferApplied = applyCommittedMirrorDelta(structuredClone(deltaBase), transferDelta);
assert(transferDelta.sourceMapDelta === materializedDelta.sourceMapDelta
&& transferDelta.metaDelta === materializedDelta.metaDelta
&& transferDelta.fields !== materializedDelta.fields
&& transferDelta.fields.field.rows[0].values.buffer !== materializedDelta.fields.field.rows[0].values.buffer
&& transferDelta.generatedMask.rows[0].values.buffer !== materializedDelta.generatedMask.rows[0].values.buffer
&& hashCommittedWorld(transferApplied) === hashCommittedWorld(deltaTarget),
"main transfer delta copies detachable raster rows only and leaves metadata for one postMessage clone");
const transportDebugWorld = {
width: 2, height: 2, originX: 0, originY: 0, renderRevision: 1,
fields: { sea: new Uint8Array(4), slope: new Float32Array(4), plain: new Float32Array(4) },
generatedRects: [],
sourceMap: {
transportDebug: {
layers: {
expresswayPotential: new Float32Array([0.5]),
components: [{ mode: "rail", cells: [[0, 0]] }],
},
},
},
};
const transportDebugViewport = getViewportMap(transportDebugWorld, { x: 0, y: 0 }, 2, 2);
assert(!ArrayBuffer.isView(transportDebugViewport.transportDebug.layers.expresswayPotential)
&& transportDebugViewport.transportDebug.layers.components.length === 1,
"viewport drops unused transport potential rasters while preserving vector diagnostics");
const arrayDeltaBase = {
width: 1, height: 1,
fields: { marker: new Uint8Array([1]) }, generatedMask: new Uint8Array(1),
sourceMap: {
roads: [{ id: "keep-a" }, { id: "replace" }, { id: "keep-b" }],
annotated: Object.assign([{ id: "old" }], { patchGenerated: true }),
},
};
const arrayDeltaTarget = structuredClone(arrayDeltaBase);
arrayDeltaTarget.sourceMap.roads.splice(1, 1, { id: "new-1" }, { id: "new-2" });
arrayDeltaTarget.sourceMap.annotated = Object.assign([{ id: "new" }], { patchGenerated: true });
const exactArrayDelta = buildCommittedMirrorDelta(arrayDeltaBase, arrayDeltaTarget);
const exactArrayApplied = applyCommittedMirrorDelta(structuredClone(arrayDeltaBase), exactArrayDelta);
assert(exactArrayDelta.sourceMapDelta?.arraySplices?.roads?.start === 1
&& exactArrayDelta.sourceMapDelta?.arraySplices?.roads?.deleteCount === 1
&& exactArrayDelta.sourceMapDelta?.arraySplices?.roads?.items?.length === 2
&& Array.isArray(exactArrayDelta.sourceMapDelta?.set?.annotated)
&& hashCommittedWorld(exactArrayApplied) === hashCommittedWorld(arrayDeltaTarget),
"metadata delta uses exact middle splices for dense layers and full replacement for annotated arrays");
const oneShotArrayDelta = buildCommittedMirrorDelta(arrayDeltaBase, arrayDeltaTarget);
const oneShotInsertedRoad = oneShotArrayDelta.sourceMapDelta.arraySplices.roads.items[0];
const oneShotArrayApplied = applyCommittedMirrorDelta(structuredClone(arrayDeltaBase), oneShotArrayDelta, { consumeMetadata: true });
assert(oneShotArrayApplied.sourceMap.roads[1] === oneShotInsertedRoad
&& hashCommittedWorld(oneShotArrayApplied) === hashCommittedWorld(arrayDeltaTarget),
"one-shot Worker/main delta application adopts its isolated metadata graph without a second full clone");
const transactionBase = {
seed: 1, width: 8, height: 6, originX: 0, originY: 0, sourceWidth: 8, sourceHeight: 6, seaLevel: 0.3,
fields: {
sea: new Uint8Array(48), elevation: new Float32Array(48), municipalityId: new Int32Array(48),
flowTo: Int32Array.from({ length: 48 }, (_, index) => index - 1),
},
generatedMask: new Uint8Array(48), sourceMap: {
villages: [{ x: 1, y: 1 }],
adminDebug: { compartmentBorders: [[[0, 0], [1, 1]]] },
terrainTemplate: { immutableReference: { label: "production terrain" } },
stable: [1, 2],
},
generatedRects: [{
x0: 0, y0: 0, x1: 8, y1: 6,
generatedFootprint: { x0: 0, y0: 0, x1: 8, y1: 6, rowRuns: [[0, 8], [0, 8]] },
}],
lastPatchResult: { ok: true, seamDiagnostics: { hardPass: true, issuePoints: [] } },
patchGenerationSerial: 0,
};
transactionBase.fields.municipalityId.fill(-1);
const committedGeneratedRectsRef = transactionBase.generatedRects;
const committedLastPatchResultRef = transactionBase.lastPatchResult;
const transactionOriginal = structuredClone(transactionBase);
const transaction = capturePatchTransactionSnapshot(transactionBase, { lightweight: false });
assert(transaction.sourceMap.terrainTemplate === transactionBase.sourceMap.terrainTemplate
&& transaction.sourceMap.stable === transactionBase.sourceMap.stable
&& transaction.sourceMap.villages !== transactionBase.sourceMap.villages
&& transaction.sourceMap.villages[0] !== transactionBase.sourceMap.villages[0]
&& transaction.sourceMap.adminDebug !== transactionBase.sourceMap.adminDebug
&& transaction.generatedRects === committedGeneratedRectsRef
&& transaction.lastPatchResult === committedLastPatchResultRef,
"transaction source snapshots share read-only roots and own every patch-mutable root");
const isolatedSourceWorld = structuredClone(transactionOriginal);
const isolatedCommittedSourceRef = isolatedSourceWorld.sourceMap;
const isolatedCommittedHash = hashCommittedWorld(isolatedSourceWorld);
const isolatedSourceTransaction = capturePatchTransactionSnapshot(isolatedSourceWorld, {
lightweight: false,
isolateSourceMap: true,
});
const isolatedCandidatePoint = { x: 6, y: 4, name: "candidate-only" };
isolatedSourceWorld.sourceMap.villages.push(isolatedCandidatePoint);
const isolatedSourceDelta = buildCommittedMirrorDeltaFromTransaction(isolatedSourceTransaction, isolatedSourceWorld);
restorePatchTransactionSnapshot(isolatedSourceWorld, isolatedSourceTransaction);
assert(isolatedSourceTransaction.sourceMap === isolatedCommittedSourceRef
&& isolatedSourceWorld.sourceMap === isolatedCommittedSourceRef
&& isolatedSourceTransaction.sourceMapIsolated === true
&& isolatedSourceDelta.sourceMapDelta.arraySplices.villages.items[0] === isolatedCandidatePoint
&& hashCommittedWorld(isolatedSourceWorld) === isolatedCommittedHash,
"Worker transactions mutate an isolated sourceMap and rollback by restoring the untouched committed reference");
preparePatchTransactionFields(transaction, transactionBase, { x0: 2, y0: 1, x1: 6, y1: 5 });
transactionBase.fields.sea[19] = 1;
transactionBase.fields.elevation[28] = 0.75;
transactionBase.fields.municipalityId[10] = 42;
transactionBase.generatedMask[19] = 1;
const acceptedVillageRef = { x: 3, y: 2 };
transactionBase.sourceMap.villages.push(acceptedVillageRef);
transactionBase.sourceMap.adminDebug.compartmentBorders.push([[2, 2], [3, 3]]);
const sharedSeamDiagnostic = { hardPass: true, issuePoints: [{ x: 3, y: 2 }] };
transactionBase.sourceMap.patchSeamDiagnostics = sharedSeamDiagnostic;
transactionBase.generatedRects = [...transactionBase.generatedRects, { x0: 2, y0: 1, x1: 6, y1: 5 }];
transactionBase.lastPatchResult = { ok: true, seamDiagnostics: sharedSeamDiagnostic };
transactionBase.patchGenerationSerial = 1;
const transactionTarget = structuredClone(transactionBase);
const transactionDelta = buildCommittedMirrorDeltaFromTransaction(transaction, transactionBase);
restorePatchTransactionSnapshot(transactionBase, transaction);
const transactionApplied = applyCommittedMirrorDelta(structuredClone(transactionOriginal), transactionDelta);
assert(JSON.stringify(transactionBase) === JSON.stringify(transactionOriginal), "transactional candidate restores the committed mirror exactly");
assert(transactionBase.generatedRects === committedGeneratedRectsRef
&& transactionBase.lastPatchResult === committedLastPatchResultRef,
"transaction rollback restores immutable history and prior diagnostics by reference without cloning them");
const regenerationMaskWorld = structuredClone(transactionOriginal);
const regenerationMaskRef = regenerationMaskWorld.generatedMask;
const regenerationMaskTransaction = capturePatchTransactionSnapshot(regenerationMaskWorld, {
lightweight: false,
copyGeneratedMask: false,
});
regenerationMaskWorld.generatedMask = new Uint8Array(regenerationMaskWorld.generatedMask.length).fill(1);
const regenerationMaskDelta = buildCommittedMirrorDeltaFromTransaction(regenerationMaskTransaction, regenerationMaskWorld);
restorePatchTransactionSnapshot(regenerationMaskWorld, regenerationMaskTransaction);
assert(regenerationMaskTransaction.generatedMask === null
&& regenerationMaskTransaction.generatedMaskRef === regenerationMaskRef
&& regenerationMaskDelta.generatedMask?.rows?.length > 0
&& regenerationMaskWorld.generatedMask === regenerationMaskRef,
"Regeneration transactions retain the read-only generated mask by reference and still detect array replacement");
assert(JSON.stringify(transactionApplied) === JSON.stringify(transactionTarget), "transaction snapshot delta exactly materializes the accepted preview");
assert(transactionDelta.sourceMapDelta.arraySplices.villages.items[0] === acceptedVillageRef,
"transaction delta adopts completed candidate metadata directly instead of cloning it before rollback");
assert(transactionDelta.previewDelta.changedCells === 3
&& transactionDelta.previewDelta.terrainChangedCells === 2
&& transactionDelta.previewDelta.adminChangedCells === 1
&& transactionDelta.previewDelta.featureLayersChanged === 1,
"transaction delta reuses its field comparison to produce exact preview statistics");
assert(transactionDelta.sourceMapDelta.set.patchSeamDiagnostics
=== transactionDelta.metaDelta.set.lastPatchResult.seamDiagnostics
&& transactionApplied.sourceMap.patchSeamDiagnostics
=== transactionApplied.lastPatchResult.seamDiagnostics,
"delta build and apply preserve shared diagnostic objects instead of cloning them per metadata key");
const transactionalCalls = [];
const transactionalSearchBase = structuredClone(transactionOriginal);
const transactionalSearch = runPatchCandidateSearch({
id: 3,
world: transactionalSearchBase,
rect: { x0: 2, y0: 1, x1: 6, y1: 5 },
options: {},
search: { candidatePlan: [{ variant: 8, seed: 108 }, { variant: 9, seed: 109 }] },
}, {
transactional: true,
generateCandidate: (world, rect, options) => {
transactionalCalls.push([world.fields.sea[19], world.fields.municipalityId[10], world.sourceMap.villages.length]);
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 options.variant === 8
? { ok: false, code: "patch-quality-gate-failed", reason: "forced reject" }
: { ok: true, seamDiagnostics: { hardPass: true }, rects: { writeRect: rect } };
},
});
assert(transactionalSearch.result?.ok && !transactionalSearch.world && transactionalSearch.transactionDelta,
"production candidate search returns a bounded delta instead of a second full world");
assert(transactionalSearch.result.previewDelta?.changedCells === 2
&& transactionalSearch.result.previewDelta?.featureLayersChanged === 1,
"transactional search returns precomputed preview statistics without a main-thread field rescan");
assert(transactionalCalls.every(([sea, municipality, villages]) => sea === 0 && municipality === -1 && villages === 1)
&& JSON.stringify(transactionalSearchBase) === JSON.stringify(transactionOriginal),
"rejected and accepted transactional candidates both start from and restore the same committed mirror");
const municipalityWorld = {
width: 8, height: 6,
fields: {
adminId: Int32Array.from({ length: 48 }, (_, index) => index % 5),
municipalityId: new Int32Array(48).fill(99),
sea: new Uint8Array(48),
},
};
const municipalityRects = {
patchMode: "regeneration",
coreRect: { x0: 2, y0: 1, x1: 6, y1: 5 },
writeRect: { x0: 2, y0: 1, x1: 6, y1: 5 },
repairRect: { x0: 0, y0: 0, x1: 8, y1: 6 },
writeMargin: 1,
};
const municipalityBefore = new Int32Array(municipalityWorld.fields.municipalityId);
const municipalityWrites = synchronizePatchMunicipalityField(municipalityWorld, municipalityRects, 17);
let municipalityOutsideChanged = 0;
for (let y = 0; y < municipalityWorld.height; y++) {
for (let x = 0; x < municipalityWorld.width; x++) {
if (x >= 2 && x < 6 && y >= 1 && y < 5) continue;
const index = y * municipalityWorld.width + x;
if (municipalityWorld.fields.municipalityId[index] !== municipalityBefore[index]) municipalityOutsideChanged++;
}
}
assert(municipalityWrites > 0 && municipalityOutsideChanged === 0,
"Regeneration municipality coherence writes only the owned patch alpha instead of rewriting the full world");
const adminWorldBase = {
seed: 3, width: 6, height: 4, originX: 0, originY: 0, sourceWidth: 6, sourceHeight: 4,
fields: {
adminId: Int32Array.from({ length: 24 }, (_, index) => index % 6 < 3 ? 0 : 1),
municipalityId: new Int32Array(24).fill(-1),
prefectureRegionId: Int32Array.from({ length: 24 }, (_, index) => index % 6 < 3 ? 10 : 11),
sea: new Uint8Array(24), populationDensity: new Float32Array(24).fill(0.5),
plain: new Float32Array(24).fill(0.6), agriculture: new Float32Array(24).fill(0.2),
slope: new Float32Array(24), ridgeField: new Float32Array(24), landuse: new Uint8Array(24),
},
generatedMask: new Uint8Array(24), generatedRects: [],
sourceMap: {
adminCenters: [], prefectureRegions: [], municipalityToPrefectureId: new Int32Array(2).fill(-1),
modernCities: [{ x: 1, y: 1, population: 1000 }, { x: 4, y: 1, population: 900 }],
},
patchGenerationSerial: 0,
};
const adminFullSecond = structuredClone(adminWorldBase);
const adminOptimizedSecond = structuredClone(adminWorldBase);
const adminFirstFull = synchronizePatchAdministrativeMetadata(adminFullSecond, adminFullSecond.sourceMap, 3, { writeMunicipalityField: true });
const adminFirstOptimized = synchronizePatchAdministrativeMetadata(adminOptimizedSecond, adminOptimizedSecond.sourceMap, 3, { writeMunicipalityField: true });
adminFullSecond.sourceMap.modernCities[0].isPrefecturalCapital = true;
adminOptimizedSecond.sourceMap.modernCities[0].isPrefecturalCapital = true;
synchronizePatchAdministrativeMetadata(adminFullSecond, adminFullSecond.sourceMap, 3, { writeMunicipalityField: false });
refreshPatchPrefectureMetadata(adminOptimizedSecond, adminOptimizedSecond.sourceMap, adminFirstOptimized.municipalCoherence, { afterCapitalNormalization: true });
assert(hashCommittedWorld(adminOptimizedSecond) === hashCommittedWorld(adminFullSecond)
&& adminFirstFull.municipalCoherence.debug.activeMunicipalities === adminFirstOptimized.municipalCoherence.debug.activeMunicipalities,
"post-capital prefecture refresh matches the former second full municipal scan exactly");
const capitalWorld = {
width: 6, height: 2, originX: 0, originY: 0,
fields: {
prefectureRegionId: Int32Array.from([0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2]),
sea: new Uint8Array(12),
populationDensity: Float32Array.from([1, 3, 1, 4, 1, 5, 2, 2, 3, 2, 4, 3]),
habitability: new Float32Array(12), slope: new Float32Array(12),
},
};
const capitalSource = { modernCities: [], markets: [], adminCenters: [] };
const capitalDebug = normalizePatchPrefectureCapitals(capitalWorld, capitalSource);
assert(capitalDebug.activePrefectures === 3 && capitalDebug.fallbackPrefectureCapitalCitiesAdded === 3
&& capitalSource.modernCities.map((city) => `${city.worldX},${city.worldY}`).join("|") === "1,0|3,0|5,0",
"capital fallback collects every prefecture's best cell in one world pass with stable tie order");
const tilePlanningWorld = { width: 1000, height: 700, originX: 0, originY: 0 };
// Expansion implementation tiles are selection-anchored so a maximum visible
// request does not generate thin world-grid edge candidates. Each tile still
// maps into one complete MAP_W x MAP_H production candidate; Regeneration keeps
// its historical full-candidate world-grid assignment.
const tilePlanningSelection = { x0: 205, y0: 20, x1: 205 + 470, y1: 20 + 333 };
const expansionTiles = buildLargeExpansionTiles(tilePlanningSelection, tilePlanningWorld);
const regenerationTiles = buildLargeExpansionTiles(tilePlanningSelection, tilePlanningWorld, {
_largeSelectionThresholdWidth: MAP_W,
_largeSelectionThresholdHeight: MAP_H,
_tileCoreWidth: MAP_W,
_tileCoreHeight: MAP_H,
});
assert(expansionTiles.length === 4
&& expansionTiles.every((tile) => tile._candidateWindowOverride?.width === MAP_W && tile._candidateWindowOverride?.height === MAP_H)
&& regenerationTiles.every((tile) => tile.x1 - tile.x0 <= MAP_W && tile.y1 - tile.y0 <= MAP_H),
"large Expansion packs a maximum visible selection into four full-production candidates while Regeneration retains full-size cores");
const standaloneSafeSelection = { x0: 200, y0: 200, x1: 320, y1: 290 };
const intermediateCoverageSelection = { x0: 200, y0: 200, x1: 360, y1: 310 };
const standaloneSafeTiles = buildLargeExpansionTiles(standaloneSafeSelection, tilePlanningWorld);
const intermediateCoverageTiles = buildLargeExpansionTiles(intermediateCoverageSelection, tilePlanningWorld);
assert(standaloneSafeTiles.length === 0
&& intermediateCoverageTiles.length === 1
&& intermediateCoverageTiles[0]._candidateWindowOverride?.width === MAP_W
&& intermediateCoverageTiles[0]._candidateWindowOverride?.height === MAP_H,
"Expansion tiling starts when the exact standalone write footprint no longer fits one production candidate, including the former 160x110 coverage hole");
let uncoveredStandaloneGeometry = null;
for (const [width, height] of [[48, 48], [96, 72], [120, 90], [128, 96], [132, 98], [160, 110], [220, 150], [242, 167]]) {
for (const [x0, y0] of [[0, 0], [200, 180], [1000 - width, 700 - height]]) {
const selection = { x0, y0, x1: x0 + width, y1: y0 + height };
if (buildLargeExpansionTiles(selection, tilePlanningWorld).length > 0) continue;
const rects = buildPatchRects(selection, tilePlanningWorld, { patchMode: "expansion", _geometryOnly: true });
const cx = (rects.coreRect.x0 + rects.coreRect.x1 - 1) / 2;
const cy = (rects.coreRect.y0 + rects.coreRect.y1 - 1) / 2;
const sourceCenterX = (MAP_W - 1) / 2;
const sourceCenterY = (MAP_H - 1) / 2;
const corners = [
[rects.writeRect.x0, rects.writeRect.y0],
[rects.writeRect.x1 - 1, rects.writeRect.y1 - 1],
];
const covered = corners.every(([x, y]) => {
const sx = Math.round(x - cx + sourceCenterX);
const sy = Math.round(y - cy + sourceCenterY);
return sx >= 0 && sx < MAP_W && sy >= 0 && sy < MAP_H;
});
if (!covered) uncoveredStandaloneGeometry = { width, height, x0, y0, writeRect: rects.writeRect };
}
}
assert(uncoveredStandaloneGeometry === null,
"every Expansion that remains on the standalone fast path has complete fixed-candidate coverage, including world-edge placements");
const rasterPartitionLasso = {
kind: "lasso",
polygon: [
{ x: 120, y: 40 },
{ x: 399, y: 60 },
{ x: 250, y: 140 },
{ x: 370, y: 239 },
{ x: 100, y: 210 },
],
};
const rasterPartitionTiles = buildLargeExpansionTiles(rasterPartitionLasso, tilePlanningWorld);
let partitionMissing = 0;
let partitionExtra = 0;
let partitionDuplicate = 0;
for (let y = 40; y < 240; y++) {
for (let x = 100; x < 400; x++) {
const selected = testPointInPolygon(x + 0.5, y + 0.5, rasterPartitionLasso.polygon);
let tileHits = 0;
for (const tile of rasterPartitionTiles) {
if (x < tile.x0 || x >= tile.x1 || y < tile.y0 || y >= tile.y1) continue;
if (testPointInPolygon(x + 0.5, y + 0.5, tile.polygon)) tileHits++;
}
if (selected && tileHits === 0) partitionMissing++;
if (!selected && tileHits > 0) partitionExtra++;
if (tileHits > 1) partitionDuplicate++;
}
}
assert(rasterPartitionTiles.length >= 2
&& partitionMissing === 0
&& partitionExtra === 0
&& partitionDuplicate === 0
&& rasterPartitionTiles.every((tile) => tile.partitionBounds && tile.areaCells > 0),
"large lasso tiles partition the original even-odd raster exactly without missing, extra, or duplicate seam cells");
const strictMetadataWorld = { width: 8, height: 6, originX: 0, originY: 0 };
const strictMetadataSource = {
modernCities: [{ x: 0, y: 0, name: "outside" }, { x: 3, y: 2, name: "inside" }],
adminCenters: [{ x: 0, y: 0, adminId: 1, name: "office" }],
prefectureRegions: [{ x: 0, y: 0, prefectureRegionId: 2, name: "region" }],
};
const strictMetadataBaseline = structuredClone(strictMetadataSource);
const strictMetadataSnapshot = captureStrictMetadataSnapshot(strictMetadataWorld, strictMetadataSource, municipalityRects, 17, strictMetadataBaseline);
assert(strictMetadataSnapshot.outsidePointLayers.get("modernCities")[0] === strictMetadataBaseline.modernCities[0]
&& strictMetadataSnapshot.byLayer.get("adminCenters")[0] === strictMetadataSnapshot.allByLayerId.get("adminCenters").get(1)
&& strictMetadataSnapshot.byLayer.get("prefectureRegions")[0] === strictMetadataSnapshot.allByLayerId.get("prefectureRegions").get(2),
"strict metadata snapshots reuse the immutable transaction before-image instead of cloning the same points into multiple stores");
const strictFieldWorld = structuredClone(transactionOriginal);
const strictFieldTransaction = capturePatchTransactionSnapshot(strictFieldWorld, { lightweight: false });
preparePatchTransactionFields(strictFieldTransaction, strictFieldWorld, municipalityRects.repairRect);
const strictFieldSnapshot = captureStrictSelectionFieldSnapshot(strictFieldWorld, municipalityRects, 17, {
deferGlobalSnapshot: true,
transactionSnapshot: strictFieldTransaction,
});
assert(strictFieldSnapshot.transactionSnapshot === strictFieldTransaction
&& strictFieldSnapshot.fieldNames.size > 0
&& strictFieldSnapshot.fields.size === 0
&& !strictFieldTransaction.fields.has("flowTo")
&& !strictFieldSnapshot.fieldNames.has("flowTo")
&& !("globalFields" in strictFieldTransaction)
&& strictFieldSnapshot.protectedIndexLookup === null,
"strict field restore reuses local transaction before-images without read-only or whole-world duplicates");
const rawFlagWorld = {
seed: 7, width: 900, height: 700, originX: 0, originY: 0,
fields: { sea: new Uint8Array(900 * 700), elevation: new Float32Array(900 * 700) },
sourceMap: {}, generatedMask: new Uint8Array(900 * 700),
};
// Half of this internal tile already exists in the committed world. Large
// Expansion human-density completion must therefore target only the genuinely
// new fraction rather than treating the entire raw production frame as new.
for (let y = 0; y < rawFlagWorld.height; y++) {
for (let x = 0; x < 225; x++) rawFlagWorld.generatedMask[y * rawFlagWorld.width + x] = 1;
}
const internalExpansionRaw = buildRawPatchCandidateRequest(rawFlagWorld, { x0: 150, y0: 120, x1: 300, y1: 226 }, {
patchMode: "expansion", _internalTile: true, seed: 701, variant: 0,
});
const ordinaryExpansionRaw = buildRawPatchCandidateRequest(rawFlagWorld, { x0: 150, y0: 120, x1: 300, y1: 226 }, {
patchMode: "expansion", seed: 701, variant: 0,
});
assert(internalExpansionRaw.ok && internalExpansionRaw.mapOptions.largeExpansionTile === true
&& ordinaryExpansionRaw.ok && ordinaryExpansionRaw.mapOptions.largeExpansionTile === false,
"canonical internal Expansion tiles activate the bounded large-tile transport policy without affecting ordinary candidates");
assert(internalExpansionRaw.mapOptions.patchHumanFocusPolygon?.length >= 3
&& Math.abs(internalExpansionRaw.mapOptions.patchHumanExpansionFraction - 0.5) < 1e-9
&& ordinaryExpansionRaw.mapOptions.patchHumanExpansionFraction == null,
"large Expansion raw candidates carry a selection focus and immutable ungenerated fraction without leaking it into ordinary candidates");
const savedWorker = globalThis.Worker;
const fakeStarts = [];
const fakeFinishes = [];
let fakeActive = 0;
let fakePeak = 0;
class FakeRawCandidateWorker {
constructor() {
this.onmessage = null;
this.onerror = null;
this.terminated = false;
}
postMessage(message) {
const index = Number(message.mapOptions?.testIndex || 0);
const delay = Number(message.mapOptions?.testDelay || 0);
fakeStarts.push({ index, at: performance.now() });
fakeActive++;
fakePeak = Math.max(fakePeak, fakeActive);
setTimeout(() => {
if (this.terminated) return;
fakeActive--;
fakeFinishes.push({ index, at: performance.now() });
this.onmessage?.({ data: {
type: "raw-patch-candidate-result",
id: message.id,
ok: true,
candidate: { index },
} });
}, delay);
}
terminate() {
this.terminated = true;
return Promise.resolve();
}
}
globalThis.Worker = FakeRawCandidateWorker;
try {
const requests = [70, 5, 5, 5, 5, 5].map((delay, index) => ({
seed: index + 1,
taskId: `scheduler-${index}`,
mapOptions: { testIndex: index, testDelay: delay },
}));
const sequence = scheduleRawCandidateSequence(requests, null, { parallelism: 2, windowSize: 3 });
await sequence.promises[1];
await new Promise((resolve) => setTimeout(resolve, 15));
assert(fakeStarts.some((entry) => entry.index === 2) && !fakeFinishes.some((entry) => entry.index === 0),
"continuous two-worker scheduler lets a free lane start tile 3 while tile 1 is a straggler");
assert(!fakeStarts.some((entry) => entry.index === 3),
"raw candidate prefetch is bounded to one lookahead beyond the two active workers before merge-head release");
const merged = [];
for (let index = 0; index < sequence.promises.length; index++) {
const candidate = await sequence.promises[index];
merged.push(candidate.index);
sequence.promises[index] = null;
sequence.release(index);
}
await sequence.done;
assert(merged.join(",") === "0,1,2,3,4,5" && fakePeak <= 2 && fakeStarts.length === 6,
"raw candidates still merge deterministically in ordinal order with at most two helper workers");
} finally {
await shutdownRawCandidateWorkers();
if (savedWorker === undefined) delete globalThis.Worker;
else globalThis.Worker = savedWorker;
}
const syntheticRawCandidate = {
baseSeed: 77,
width: MAP_W,
height: MAP_H,
sea: new Uint8Array([0, 1, 0, 1]),
municipalityToPrefectureId: new Int32Array([0, 1, 1]),
villages: [{ x: 2, y: 3 }],
nationalRoads: [[[0, 0], [1, 1]]],
generationContext: { width: MAP_W, height: MAP_H },
terrainTemplate: { terrainType: "test" },
adminDebug: { compartmentBorders: [] },
geography: { veryLargeGraph: new Array(500).fill({ id: 1 }) },
naturalCompartments: [{ id: 1 }],
unrelatedDebug: { shouldDrop: true },
};
const compactedRawCandidate = compactRawPatchCandidate(syntheticRawCandidate);
const compactedRawSummary = summarizeRawPatchCandidate(compactedRawCandidate);
assert(compactedRawCandidate.sea === syntheticRawCandidate.sea
&& compactedRawCandidate.municipalityToPrefectureId === syntheticRawCandidate.municipalityToPrefectureId
&& compactedRawCandidate.villages === syntheticRawCandidate.villages
&& compactedRawCandidate.nationalRoads === syntheticRawCandidate.nationalRoads
&& compactedRawCandidate.generationContext === syntheticRawCandidate.generationContext,
"raw-candidate compaction preserves dynamically discovered raster fields and merge-required vector metadata");
assert(!("geography" in compactedRawCandidate)
&& !("naturalCompartments" in compactedRawCandidate)
&& !("unrelatedDebug" in compactedRawCandidate)
&& compactedRawSummary.typedArrayCount === 2
&& compactedRawSummary.transferableBytes === syntheticRawCandidate.sea.byteLength + syntheticRawCandidate.municipalityToPrefectureId.byteLength,
"raw-candidate compaction drops full-map-only graphs without hiding transferable-byte accounting");
const terrainRehomeWorld = {
width: 24, height: 24, originX: 0, originY: 0,
fields: {
sea: new Uint8Array(24 * 24).fill(1),
slope: new Float32Array(24 * 24),
adminId: new Int32Array(24 * 24).fill(-1),
},
};
for (let y = 8; y <= 14; y++) for (let x = 8; x <= 14; x++) {
terrainRehomeWorld.fields.sea[y * 24 + x] = 0;
terrainRehomeWorld.fields.adminId[y * 24 + x] = 7;
}
const terrainRehomeSource = {
villages: [
{ x: 6, y: 10, adminId: 7, patchGenerated: true },
{ x: 2, y: 2, adminId: 3, patchGenerated: false },
],
};
const terrainRehomeRects = {
coreRect: { x0: 4, y0: 4, x1: 20, y1: 20 },
writeRect: { x0: 4, y0: 4, x1: 20, y1: 20 },
patchMode: "expansion",
expansionOverlap: 0,
};
const terrainRehomeDebug = reconcileGeneratedHumanPointsWithFinalTerrain(
terrainRehomeWorld, terrainRehomeSource, terrainRehomeRects, 123
);
assert(terrainRehomeDebug.relocated === 1 && terrainRehomeDebug.dropped === 0
&& terrainRehomeSource.villages.length === 2
&& terrainRehomeWorld.fields.sea[terrainRehomeSource.villages[0].y * 24 + terrainRehomeSource.villages[0].x] === 0
&& terrainRehomeSource.villages[1].x === 2 && terrainRehomeSource.villages[1].y === 2,
"final coastline reconciliation relocates only current generated settlements from water to owned land and preserves legacy points");
const tiledQualityBasis = buildTiledFinalQualityBasis([
{ candidateQuality: { terrain: { terrainType: "auto" }, human: {
minLabels: 30, minSettlements: 18, minAdminCenters: 2, transportRequired: true,
labelCount: 24, settlementCount: 15, counts: { adminCenters: 2 },
} } },
{ candidateQuality: { terrain: { terrainType: "auto" }, human: {
minLabels: 20, minSettlements: 12, minAdminCenters: 1, transportRequired: false,
labelCount: 17, settlementCount: 11, counts: { adminCenters: 1 },
} } },
], "auto");
assert(tiledQualityBasis.human.minLabels === 50
&& tiledQualityBasis.human.minSettlements === 30
&& tiledQualityBasis.human.preMergeLabelCount === 41
&& tiledQualityBasis.human.preMergeSettlementCount === 26
&& tiledQualityBasis.human.minAdminCenters === 3
&& tiledQualityBasis.human.preMergeAdminCenterCount === 3
&& tiledQualityBasis.human.transportRequired === true,
"whole-selection tiled quality basis retains both theoretical floors and actual pre-merge human-geography counts");
const sourcePruneInitial = {
seed: 1,
sea: new Uint8Array(MAP_W * MAP_H),
elevation: new Float32Array(MAP_W * MAP_H),
geography: { cells: [1, 2, 3] },
naturalCompartments: [{ id: 1 }],
geographicCompartmentProfiles: [{ id: 1 }],
watershedProfiles: [{ id: 1 }],
entitiesForNames: [{ id: 1 }],
geographyDebug: { heavy: true },
transportDebug: { keep: true },
generationTimings: [{ key: "terrain", ms: 1 }],
villages: [{ x: 1, y: 1 }],
};
const sourcePruneWorld = createWorldMap(sourcePruneInitial, { paddingX: 0, paddingY: 0 });
assert(!("geography" in sourcePruneWorld.sourceMap)
&& !("naturalCompartments" in sourcePruneWorld.sourceMap)
&& !("geographicCompartmentProfiles" in sourcePruneWorld.sourceMap)
&& !("watershedProfiles" in sourcePruneWorld.sourceMap)
&& !("entitiesForNames" in sourcePruneWorld.sourceMap)
&& !("geographyDebug" in sourcePruneWorld.sourceMap),
"committed sourceMap prunes generator-only full-map graphs after world materialization");
assert(sourcePruneWorld.sourceMap.transportDebug === sourcePruneInitial.transportDebug
&& sourcePruneWorld.sourceMap.generationTimings === sourcePruneInitial.generationTimings
&& sourcePruneWorld.sourceMap.villages === sourcePruneInitial.villages,
"committed sourceMap retains runtime diagnostics and vector layers that viewport/patch logic still consumes");
const memo = createExactNoiseMemo();
let mismatches = 0;
for (let index = 0; index < 2000; index++) {
const x = (index * 17 % 997) - 480.25;
const y = (index * 43 % 1231) - 610.75;
const seed = (114514 + index * 101) >>> 0;
const scale = 3.75 + (index % 53);
if (memo.valueNoise(x, y, seed, scale) !== valueNoise(x, y, seed, scale)) mismatches++;
if (memo.fbm(x, y, seed) !== fbm(x, y, seed)) mismatches++;
}
assert(mismatches === 0, "exact terrain noise memo preserves every sampled value and octave order");
console.log("All additional-generation unit tests passed.");

View file

@ -0,0 +1,19 @@
import { parentPort, workerData } from "node:worker_threads";
if (!parentPort) throw new Error("browser-nested-worker-node-shim requires a parent port.");
if (!workerData?.moduleUrl) throw new Error("browser-nested-worker-node-shim requires moduleUrl.");
globalThis.self = {
onmessage: null,
postMessage(message, transfer = []) {
parentPort.postMessage(message, transfer);
},
};
await import(workerData.moduleUrl);
if (typeof globalThis.self.onmessage !== "function") {
throw new Error(`Nested browser worker module did not install onmessage: ${workerData.moduleUrl}`);
}
parentPort.on("message", (data) => globalThis.self.onmessage({ data }));

View file

@ -0,0 +1,41 @@
import { parentPort, Worker as NodeWorker } from "node:worker_threads";
if (!parentPort) throw new Error("browser-worker-node-shim requires a worker_threads parent port.");
class BrowserStyleNestedWorker {
constructor(url) {
const moduleUrl = url instanceof URL ? url.href : new URL(String(url), import.meta.url).href;
this.onmessage = null;
this.onerror = null;
this._worker = new NodeWorker(new URL("./browser-nested-worker-node-shim.mjs", import.meta.url), {
type: "module",
workerData: { moduleUrl },
});
this._worker.on("message", (data) => this.onmessage?.({ data }));
this._worker.on("error", (error) => this.onerror?.({ message: error?.message || String(error), error }));
}
postMessage(message, transfer = []) {
this._worker.postMessage(message, transfer);
}
terminate() {
return this._worker.terminate();
}
}
globalThis.Worker = BrowserStyleNestedWorker;
globalThis.self = {
onmessage: null,
postMessage(message, transfer = []) {
parentPort.postMessage(message, transfer);
},
};
await import("../src/mapPatchWorker.js");
if (typeof globalThis.self.onmessage !== "function") {
throw new Error("mapPatchWorker did not install its browser Worker message handler.");
}
parentPort.on("message", (data) => globalThis.self.onmessage({ data }));

296
tests/chromium-cdp-page.mjs Normal file
View file

@ -0,0 +1,296 @@
import { spawn } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
class CdpSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.nextId = 0;
this.pending = new Map();
}
async connect() {
const ws = new WebSocket(this.url);
this.ws = ws;
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`Timed out connecting to CDP target ${this.url}`)), 10_000);
ws.addEventListener("open", () => {
clearTimeout(timer);
resolve();
}, { once: true });
ws.addEventListener("error", (event) => {
clearTimeout(timer);
reject(event?.error || new Error("CDP WebSocket connection failed."));
}, { once: true });
});
ws.addEventListener("message", (event) => {
let message;
try {
message = JSON.parse(String(event.data));
} catch {
return;
}
if (message.id == null) return;
const waiter = this.pending.get(message.id);
if (!waiter) return;
this.pending.delete(message.id);
if (message.error) waiter.reject(new Error(message.error.message || JSON.stringify(message.error)));
else waiter.resolve(message.result || {});
});
ws.addEventListener("close", () => {
for (const waiter of this.pending.values()) waiter.reject(new Error("CDP target closed."));
this.pending.clear();
});
}
send(method, params = {}) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return Promise.reject(new Error(`CDP socket is not open for ${method}.`));
}
const id = ++this.nextId;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.ws.send(JSON.stringify({ id, method, params }));
});
}
close() {
try { this.ws?.close(); } catch {}
}
}
class CdpPage {
constructor(browser, target) {
this.browser = browser;
this.target = target;
this.timeoutMs = 30_000;
this.cdp = new CdpSocket(target.webSocketDebuggerUrl);
this.mouse = {
click: async (x, y) => {
const px = Number(x);
const py = Number(y);
if (!Number.isFinite(px) || !Number.isFinite(py)) {
throw new TypeError(`Invalid mouse coordinates: ${x}, ${y}`);
}
await this.cdp.send("Input.dispatchMouseEvent", {
type: "mouseMoved", x: px, y: py, button: "none", buttons: 0,
});
await this.cdp.send("Input.dispatchMouseEvent", {
type: "mousePressed", x: px, y: py, button: "left", buttons: 1, clickCount: 1,
});
await this.cdp.send("Input.dispatchMouseEvent", {
type: "mouseReleased", x: px, y: py, button: "left", buttons: 0, clickCount: 1,
});
},
};
}
async init() {
await this.cdp.connect();
await Promise.all([
this.cdp.send("Page.enable"),
this.cdp.send("Runtime.enable"),
]);
return this;
}
setDefaultTimeout(ms) {
this.timeoutMs = Number(ms) || this.timeoutMs;
}
async _runtimeEvaluate(expression) {
const result = await this.cdp.send("Runtime.evaluate", {
expression,
awaitPromise: true,
returnByValue: true,
userGesture: true,
});
if (result.exceptionDetails) {
const description = result.exceptionDetails.exception?.description
|| result.exceptionDetails.text
|| "Runtime.evaluate failed.";
throw new Error(description);
}
return result.result?.value;
}
async evaluate(fn, arg) {
if (typeof fn === "string") return this._runtimeEvaluate(fn);
if (typeof fn !== "function") throw new TypeError("page.evaluate requires a function or expression string.");
const argument = arguments.length >= 2 ? JSON.stringify(arg) : "";
const expression = argument
? `(${fn.toString()})(${argument})`
: `(${fn.toString()})()`;
return this._runtimeEvaluate(expression);
}
async goto(url, options = {}) {
const timeout = Number(options.timeout || this.timeoutMs);
const navigation = await this.cdp.send("Page.navigate", { url });
if (navigation.errorText) throw new Error(`Navigation failed: ${navigation.errorText}`);
const started = Date.now();
while (Date.now() - started < timeout) {
const ready = await this._runtimeEvaluate("document.readyState").catch(() => "loading");
if (ready === "interactive" || ready === "complete") return;
await wait(25);
}
throw new Error(`Navigation timed out after ${timeout} ms: ${url}`);
}
async waitForFunction(fn, arg, options = {}) {
const timeout = Number(options.timeout || this.timeoutMs);
const started = Date.now();
let lastError = null;
while (Date.now() - started < timeout) {
try {
if (await this.evaluate(fn, arg)) return true;
lastError = null;
} catch (error) {
lastError = error;
}
await wait(40);
}
const suffix = lastError ? ` Last evaluation error: ${lastError.message}` : "";
throw new Error(`waitForFunction timed out after ${timeout} ms.${suffix}`);
}
locator(selector) {
const page = this;
const normalized = String(selector || "");
return {
async click(options = {}) {
const timeout = Number(options.timeout || page.timeoutMs);
const started = Date.now();
let rect = null;
while (Date.now() - started < timeout) {
rect = await page.evaluate((css) => {
const element = document.querySelector(css);
if (!element) return null;
const box = element.getBoundingClientRect();
const style = getComputedStyle(element);
const disabled = Boolean(element.disabled || element.getAttribute("aria-disabled") === "true");
if (disabled || style.display === "none" || style.visibility === "hidden" || box.width <= 0 || box.height <= 0) return null;
return {
x: box.left + box.width / 2,
y: box.top + box.height / 2,
};
}, normalized).catch(() => null);
if (rect && Number.isFinite(rect.x) && Number.isFinite(rect.y)) break;
await wait(25);
}
if (!rect || !Number.isFinite(rect.x) || !Number.isFinite(rect.y)) {
throw new Error(`Unable to click ${normalized}: element was not actionable within ${timeout} ms.`);
}
// CDP Input events enter Chromium through the browser input pipeline and
// therefore produce trusted DOM events. This is materially different from
// calling element.click(), which would bypass the input-latency gate.
await page.mouse.click(rect.x, rect.y);
},
};
}
async close() {
this.cdp.close();
try {
await fetch(`${this.browser.httpOrigin}/json/close/${encodeURIComponent(this.target.id)}`);
} catch {}
}
}
class CdpBrowser {
constructor(process, userDataDir, httpOrigin) {
this.process = process;
this.userDataDir = userDataDir;
this.httpOrigin = httpOrigin;
}
async newPage() {
const response = await fetch(`${this.httpOrigin}/json/new?${encodeURIComponent("about:blank")}`, { method: "PUT" });
if (!response.ok) throw new Error(`Unable to create Chromium target: HTTP ${response.status}`);
const target = await response.json();
return new CdpPage(this, target).init();
}
async close() {
if (this.process.exitCode == null && !this.process.killed) {
try { this.process.kill("SIGTERM"); } catch {}
await Promise.race([
new Promise((resolve) => this.process.once("exit", resolve)),
wait(3000),
]);
}
if (this.process.exitCode == null && !this.process.killed) {
try { this.process.kill("SIGKILL"); } catch {}
}
await rm(this.userDataDir, { recursive: true, force: true }).catch(() => {});
}
}
export async function launchChromiumCdp({ executablePath = process.env.CHROMIUM_PATH || "/usr/bin/chromium" } = {}) {
const userDataDir = await mkdtemp(join(tmpdir(), "jmg-chromium-"));
const child = spawn(executablePath, [
"--headless=new",
"--no-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-background-networking",
// Headless targets otherwise receive Chromium's background renderer/worker
// scheduling priority. Additional-generation acceptance is defined for an
// actively used foreground map, so keep the CDP fallback at foreground
// scheduling semantics rather than benchmarking an artificially throttled
// tab.
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding",
"--no-proxy-server",
"--host-resolver-rules=MAP jmg.test 127.0.0.1",
"--disable-default-apps",
"--disable-extensions",
"--disable-sync",
"--metrics-recording-only",
"--mute-audio",
"--no-first-run",
"--enable-precise-memory-info",
"--remote-debugging-port=0",
`--user-data-dir=${userDataDir}`,
"about:blank",
], { stdio: ["ignore", "ignore", "pipe"] });
let stderr = "";
const endpoint = await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Chromium did not expose a DevTools endpoint.\n${stderr.slice(-4000)}`));
}, 15_000);
const onData = (chunk) => {
const text = chunk.toString();
stderr = `${stderr}${text}`.slice(-8000);
const match = stderr.match(/DevTools listening on ws:\/\/127\.0\.0\.1:(\d+)\//);
if (!match) return;
clearTimeout(timer);
child.stderr.off("data", onData);
resolve(`http://127.0.0.1:${match[1]}`);
};
child.stderr.on("data", onData);
child.once("error", (error) => {
clearTimeout(timer);
reject(error);
});
child.once("exit", (code) => {
if (code == null || code === 0) return;
clearTimeout(timer);
reject(new Error(`Chromium exited before CDP startup with code ${code}.\n${stderr}`));
});
}).catch(async (error) => {
try { child.kill("SIGKILL"); } catch {}
await rm(userDataDir, { recursive: true, force: true }).catch(() => {});
throw error;
});
return new CdpBrowser(child, userDataDir, endpoint);
}

View file

@ -0,0 +1,102 @@
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}`);
}
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 terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
const seed = deriveSeed(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;
let terminationMs = Infinity;
let cancellationTriggered = false;
const timeout = setTimeout(async () => {
try { await worker.terminate(); } catch {}
console.error("FAIL: production patch worker did not reach cancellable large-precompute work in time");
process.exit(2);
}, 35_000);
worker.on("message", async (message) => {
if (message.id !== 1) return;
if (message.type !== "progress") {
terminalMessage = message;
return;
}
const progress = message.progress || {};
if (cancellationTriggered || progress.phase !== "large-candidate-precompute") return;
cancellationTriggered = true;
// Let the coordinator enter nested-worker work rather than measuring an idle
// worker immediately after the phase marker.
await new Promise((resolve) => setTimeout(resolve, 100));
cancelStartedAt = performance.now();
await worker.terminate();
terminationMs = performance.now() - cancelStartedAt;
clearTimeout(timeout);
assert(terminationMs < 500, `production patch Worker termination settles within 500 ms (${terminationMs.toFixed(1)} ms)`);
// terminate() is the hard cancellation boundary. No terminal candidate can
// be published after it resolves.
await new Promise((resolve) => setTimeout(resolve, 25));
assert(terminalMessage === null, "terminated production Worker cannot publish a candidate after cancellation");
console.log("All patch-worker cancellation tests passed.");
process.exit(0);
});
worker.on("error", async (error) => {
clearTimeout(timeout);
try { await worker.terminate(); } catch {}
console.error(error?.stack || String(error));
process.exit(1);
});
worker.postMessage({
id: 1,
world,
rect,
options: {
patchMode: "expansion",
terrainType,
variant: 0,
seed,
maxQualityRetries: 0,
qualityTerrainAttempts: 1,
acceptBestAvailableQuality: false,
includeSeamVisualization: false,
},
search: {
searchId: "cancel-integration",
operationId: "cancel-integration",
committedRevision: 1,
workerEpoch: 1,
executionAttempt: 1,
totalCandidateCount: 1,
candidatePlan: [{ candidateId: "cancel:0", candidateOrdinal: 1, variant: 0, seed }],
},
});

View file

@ -0,0 +1,182 @@
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}`);
}
function isPlainObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value) || ArrayBuffer.isView(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function sourceEntryNeedsChunking(value) {
return isPlainObject(value) && Object.values(value).some((entry) => ArrayBuffer.isView(entry) || entry instanceof ArrayBuffer);
}
function manifestFor(world) {
const sourceKeys = Object.keys(world.sourceMap || {});
const expandedSourceObjects = {};
for (const key of sourceKeys) if (sourceEntryNeedsChunking(world.sourceMap[key])) expandedSourceObjects[key] = Object.keys(world.sourceMap[key]);
return {
rootKeys: Object.keys(world).filter((key) => key !== "fields" && key !== "sourceMap" && key !== "generatedMask"),
fieldKeys: Object.keys(world.fields || {}),
sourceKeys,
expandedSourceObjects,
hasGeneratedMask: !!world.generatedMask,
};
}
function transferCopy(value) {
if (ArrayBuffer.isView(value)) {
if (value instanceof DataView) {
const buffer = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
return { value: new DataView(buffer), transfer: [buffer], bytes: buffer.byteLength };
}
const copy = new value.constructor(value);
return { value: copy, transfer: [copy.buffer], bytes: copy.byteLength };
}
if (value instanceof ArrayBuffer) {
const copy = value.slice(0);
return { value: copy, transfer: [copy], bytes: copy.byteLength };
}
return { value, transfer: [], bytes: 0 };
}
function waitFor(worker, predicate, timeoutMs = 120_000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => done(new Error("Worker response timed out.")), timeoutMs);
const onMessage = (message) => { if (predicate(message)) done(null, message); };
const onError = (error) => done(error);
const done = (error, value) => {
clearTimeout(timer);
worker.off("message", onMessage);
worker.off("error", onError);
if (error) reject(error); else resolve(value);
};
worker.on("message", onMessage);
worker.on("error", onError);
});
}
async function sendSync(worker, envelope, type, payload = {}, transfer = []) {
const sequence = ++envelope.sequence;
const reply = waitFor(worker, (message) => message?.type === "patch-mirror-sync-ack"
&& message.id === envelope.id && message.syncId === envelope.syncId && message.sequence === sequence);
worker.postMessage({ id: envelope.id, type, syncId: envelope.syncId, sequence, ...payload }, transfer);
const ack = await reply;
if (!ack.ok) throw new Error(ack.error || `${type} failed`);
return ack;
}
async function main() {
const initial = generateMap(114514);
const world = createWorldMap(initial);
const originalElevationBuffer = world.fields.elevation.buffer;
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
const envelope = { id: 91, syncId: "node-cold-sync:1", sequence: 0 };
const manifest = manifestFor(world);
let maxBinaryChunkBytes = 0;
const syncStartedAt = performance.now();
try {
await sendSync(worker, envelope, "patch-mirror-sync-start", { committedRevision: 1, manifest });
for (const key of manifest.rootKeys) {
const prepared = transferCopy(world[key]);
maxBinaryChunkBytes = Math.max(maxBinaryChunkBytes, prepared.bytes);
await sendSync(worker, envelope, "patch-mirror-sync-root", { key, value: prepared.value }, prepared.transfer);
}
for (const key of manifest.fieldKeys) {
const prepared = transferCopy(world.fields[key]);
maxBinaryChunkBytes = Math.max(maxBinaryChunkBytes, prepared.bytes);
await sendSync(worker, envelope, "patch-mirror-sync-field", { key, value: prepared.value }, prepared.transfer);
}
if (manifest.hasGeneratedMask) {
const prepared = transferCopy(world.generatedMask);
maxBinaryChunkBytes = Math.max(maxBinaryChunkBytes, prepared.bytes);
await sendSync(worker, envelope, "patch-mirror-sync-generated-mask", { value: prepared.value }, prepared.transfer);
}
for (const key of manifest.sourceKeys) {
const value = world.sourceMap[key];
const childKeys = manifest.expandedSourceObjects[key];
if (Array.isArray(childKeys)) {
await sendSync(worker, envelope, "patch-mirror-sync-source-object-start", { key });
for (const childKey of childKeys) {
const prepared = transferCopy(value[childKey]);
maxBinaryChunkBytes = Math.max(maxBinaryChunkBytes, prepared.bytes);
await sendSync(worker, envelope, "patch-mirror-sync-source-object-entry", { key, childKey, value: prepared.value }, prepared.transfer);
}
} else {
const prepared = transferCopy(value);
maxBinaryChunkBytes = Math.max(maxBinaryChunkBytes, prepared.bytes);
await sendSync(worker, envelope, "patch-mirror-sync-source", { key, value: prepared.value }, prepared.transfer);
}
}
const finish = await sendSync(worker, envelope, "patch-mirror-sync-finish");
const syncMs = performance.now() - syncStartedAt;
assert(finish.mirrorCommittedRevision === 1, "cold mirror bootstrap installs the requested committed revision only after finish");
assert(world.fields.elevation.buffer === originalElevationBuffer && world.fields.elevation.byteLength > 0,
"cold mirror bootstrap transfers copies and never detaches the main committed rasters");
assert(maxBinaryChunkBytes < 1024 * 1024,
`cold mirror binary synchronization remains sub-megabyte per main-thread dispatch (${maxBinaryChunkBytes} bytes max)`);
assert(syncMs < 10_000, `cold mirror synchronization completes without a whole-world structured-clone stall (${Math.round(syncMs)} ms)`);
const rect = {
x0: world.originX + 72,
y0: world.originY + 58,
x1: world.originX + 132,
y1: world.originY + 118,
};
const candidate = { candidateId: "node-sync:1", candidateOrdinal: 1, variant: 1, seed: 0x51a7c3d3 };
const resultPromise = waitFor(worker, (message) => message?.id === 92 && message?.type !== "progress", 180_000);
worker.postMessage({
id: 92,
world: null,
rect,
options: {
patchMode: "regeneration",
terrainType: "auto",
variant: candidate.variant,
seed: candidate.seed,
maxQualityRetries: 0,
qualityTerrainAttempts: 1,
acceptBestAvailableQuality: false,
includeSeamVisualization: false,
},
search: {
searchId: "node-sync-search",
operationId: "node-sync-search",
committedRevision: 1,
workerEpoch: 1,
executionAttempt: 1,
totalCandidateCount: 1,
reuseCommittedMirror: true,
candidatePlan: [candidate],
},
});
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");
const ackId = "node-sync-apply";
const applyPromise = waitFor(worker, (message) => message?.type === "patch-apply-ack-result" && message.ackId === ackId);
worker.postMessage({
type: "patch-apply-ack",
ackId,
applyToken: result.result.applyToken,
baseCommittedRevision: 1,
committedRevision: 2,
});
const applyAck = await applyPromise;
assert(applyAck.ok === true && applyAck.mirrorCommittedRevision === 2, "Apply ACK advances the synchronized persistent mirror by exactly one revision");
assert(applyAck.mirrorHash === result.result.acceptedWorldHash, "Apply ACK mirror hash matches the accepted candidate hash");
} finally {
await worker.terminate();
}
}
await main();
console.log("All patch Worker mirror synchronization tests passed.");

View file

@ -0,0 +1,268 @@
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { extname, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const workspace = resolve(fileURLToPath(new URL("..", import.meta.url)));
const profile = process.env.BROWSER_E2E_PROFILE === "release" ? "release" : "smoke";
const configuredRepetitions = Number(process.env.BROWSER_E2E_REPETITIONS);
const repetitions = Number.isFinite(configuredRepetitions) && configuredRepetitions > 0
? Math.max(1, Math.floor(configuredRepetitions))
: profile === "release" ? 20 : 1;
const timeoutMs = profile === "release" ? 30 * 60_000 : 12 * 60_000;
const allWorkloads = [
{ name: "regeneration-rect", mode: "regeneration", shape: "rect", width: 60, height: 60 },
{ name: "expansion-lasso", mode: "expansion", shape: "lasso", width: 96, height: 72 },
{ name: "regeneration-cancel", mode: "regeneration", shape: "rect", width: 60, height: 60, kind: "cancel" },
{ name: "regeneration-large", mode: "regeneration", shape: "rect", width: 259, height: 184 },
{ name: "expansion-max-visible", mode: "expansion", shape: "lasso", width: 470, height: 333 },
];
const requestedWorkloads = new Set(String(process.env.BROWSER_E2E_WORKLOADS || "")
.split(",").map((value) => value.trim()).filter(Boolean));
const workloads = requestedWorkloads.size
? allWorkloads.filter((workload) => requestedWorkloads.has(workload.name))
: allWorkloads;
if (!workloads.length) {
throw new Error(`No browser E2E workloads matched BROWSER_E2E_WORKLOADS=${process.env.BROWSER_E2E_WORKLOADS || ""}.`);
}
const mime = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
};
function percentile95(values) {
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] || 0;
}
async function browserHeap(page, label) {
return page.evaluate((sampleLabel) => performance.memory ? {
label: sampleLabel,
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
} : null, label);
}
async function runAppWorkload(page, origin, workload) {
await page.goto(`${origin}/index.html?additionalGenerationE2E=1`, { waitUntil: "domcontentloaded", timeout: 30_000 });
await page.waitForFunction(() => window.__additionalGenerationE2E?.snapshot().ready === true, null, { timeout: timeoutMs });
const ready = await page.evaluate(() => window.__additionalGenerationE2E.snapshot());
const x0 = workload.mode === "expansion"
? Math.max(0, Math.min(ready.worldWidth - workload.width, ready.originX + 238))
: Math.max(0, Math.min(ready.worldWidth - workload.width, ready.originX));
const y0 = Math.max(0, Math.min(ready.worldHeight - workload.height, ready.originY));
const rect = { x0, y0, x1: x0 + workload.width, y1: y0 + workload.height };
if (workload.shape === "lasso") {
const insetX = Math.max(4, Math.floor(workload.width * 0.16));
const insetY = Math.max(4, Math.floor(workload.height * 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 heap = [await browserHeap(page, "ready")].filter(Boolean);
const before = await page.evaluate((config) => window.__additionalGenerationE2E.configure(config), {
rect, patchMode: workload.mode, terrainType: "auto", variant: 0, seamDiagnostics: false,
});
if (workload.kind === "cancel") {
// Resolve the physical target before generation begins. A selector lookup
// performed after dispatch can itself be delayed by a synchronous main-
// thread slice and would therefore hide the very input-latency regression
// this test is intended to detect.
const cancelPoint = await page.evaluate(() => {
const button = document.querySelector("#cancelPatchGeneration");
if (!button) throw new Error("Cancel button is missing.");
button.scrollIntoView({ block: "center", inline: "center" });
const box = button.getBoundingClientRect();
if (box.width <= 0 || box.height <= 0) throw new Error("Cancel button is not visible.");
return { x: box.left + box.width / 2, y: box.top + box.height / 2 };
});
await page.evaluate(() => { window.__additionalGenerationE2EPending = window.__additionalGenerationE2E.generate(); });
// Give the operation enough time to cross the first animation-frame yield
// and enter Worker/mirror work, but do not probe the renderer with
// Runtime.evaluate here: that probe can block behind a long task and cause
// the test to click only after generation has already finished.
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
const cancelStartedAt = performance.now();
await page.mouse.click(cancelPoint.x, cancelPoint.y);
const inputDispatchWallMs = performance.now() - cancelStartedAt;
await page.evaluate(() => window.__additionalGenerationE2EPending);
const inputToSettledWallMs = performance.now() - cancelStartedAt;
const { settled, cancelTiming } = await page.evaluate(() => ({
settled: window.__additionalGenerationE2E.snapshot(),
cancelTiming: window.__additionalGenerationE2ECancelTiming,
}));
const assertions = {
trustedPhysicalInput: cancelTiming?.isTrusted === true,
inputReachedHandlerWithinBudget: Number(cancelTiming?.inputToHandlerMs) < 500,
handlerCancelledWithinBudget: Number(cancelTiming?.handlerToCancelledMs) < 500,
inputCancelledWithinBudget: Number(cancelTiming?.inputToCancelledMs) < 500,
inputDispatchReturnedWithinBudget: inputDispatchWallMs < 500,
externalInputToSettledWithinBudget: inputToSettledWallMs < 500,
generationStopped: !settled.patchBusy && cancelTiming?.patchBusyAfter === false,
noPreviewPublished: !settled.pending,
previousCanvasPreserved: settled.canvasDigest === before.canvasDigest,
cancellationReported: /cancel/i.test(`${settled.patchStatus} ${settled.progressText}`),
};
return {
status: Object.values(assertions).every(Boolean) ? "pass" : "fail",
workload: { ...workload, selection: rect },
timing: {
patchWallMs: inputToSettledWallMs,
inputDispatchWallMs,
inputToSettledWallMs,
patchBudgetMs: 500,
patchMaxProgressGapMs: 0,
...cancelTiming,
},
memory: null,
assertions,
before,
cancelTiming,
settled,
};
}
const patchStartedAt = performance.now();
const generated = await page.evaluate(() => window.__additionalGenerationE2E.generate());
const patchWallMs = performance.now() - patchStartedAt;
const afterGenerateHeap = await browserHeap(page, "after-generate");
if (afterGenerateHeap) heap.push(afterGenerateHeap);
const applyStartedAt = performance.now();
const applied = generated.pending
? await page.evaluate(() => window.__additionalGenerationE2E.apply())
: generated;
const applyWallMs = performance.now() - applyStartedAt;
const afterApplyHeap = await browserHeap(page, "after-apply");
if (afterApplyHeap) heap.push(afterApplyHeap);
const boundedProgress = (generated.progressEvents || []).filter((event) => event.boundedWork);
const progressTimes = (generated.progressEvents || []).map((event) => Number(event.at)).filter(Number.isFinite);
let patchMaxProgressGapMs = 0;
for (let index = 1; index < progressTimes.length; index++) {
patchMaxProgressGapMs = Math.max(patchMaxProgressGapMs, progressTimes[index] - progressTimes[index - 1]);
}
const boundedProtocolByUnit = new Map();
let boundedProgressMonotonic = boundedProgress.length > 0;
for (const event of boundedProgress) {
const unitId = String(event.workUnitId || "");
const completed = Number(event.completed);
const total = Number(event.total);
if (!unitId || !Number.isFinite(completed) || !Number.isFinite(total) || completed < 0 || completed > total) {
boundedProgressMonotonic = false;
break;
}
const previous = boundedProtocolByUnit.get(unitId);
if (previous && (previous.total !== total || completed < previous.completed)) {
boundedProgressMonotonic = false;
break;
}
boundedProtocolByUnit.set(unitId, { completed, total });
}
const assertions = {
acceptedPreviewPublished: generated.pending && generated.publicationStatus === "published" && generated.searchStatus === "succeeded",
canvasChangedAtPublish: !!generated.canvasDigest && generated.canvasDigest !== before.canvasDigest,
completeStatsPublished: !!generated.statsText && !!generated.diagnosticsText,
applyCommittedOneRevision: generated.pending && applied.committedRevision === before.committedRevision + 1,
applyClearedPending: generated.pending && !applied.pending,
applyMirrorAcked: generated.pending && applied.workerMirrorRevision === applied.committedRevision,
boundedProgressValid: boundedProgressMonotonic,
patchBudgetMet: patchWallMs < 60_000,
};
return {
status: Object.values(assertions).every(Boolean) ? "pass" : "fail",
workload: { ...workload, selection: rect },
timing: { patchWallMs, applyWallMs, patchBudgetMs: 60_000, patchMaxProgressGapMs },
memory: heap.length ? { snapshots: heap, peakUsedJSHeapSize: Math.max(...heap.map((item) => item.usedJSHeapSize)) } : null,
assertions,
before,
generated,
applied,
};
}
let launchBrowser;
let browserBackend = "playwright";
try {
const playwright = await import("playwright");
launchBrowser = () => playwright.chromium.launch({ headless: true });
} catch (playwrightError) {
try {
const { launchChromiumCdp } = await import("./chromium-cdp-page.mjs");
launchBrowser = () => launchChromiumCdp();
browserBackend = "chromium-cdp";
console.error(`[browser-e2e] Playwright unavailable; using direct Chromium CDP backend (${playwrightError?.message || playwrightError}).`);
} catch (cdpError) {
console.error("Browser E2E infrastructure error: neither Playwright nor direct Chromium CDP is available.");
console.error(cdpError?.message || String(cdpError));
process.exitCode = 1;
}
}
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 file = resolve(workspace, `.${relative}`);
if (file !== workspace && !file.startsWith(`${workspace}${sep}`)) throw new Error("Path outside workspace");
const bytes = await readFile(file);
response.writeHead(200, { "content-type": mime[extname(file)] || "application/octet-stream", "cache-control": "no-store" });
response.end(bytes);
} catch (error) {
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
response.end(error?.message || "Not found");
}
});
await new Promise((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address();
// System Chromium policies in some CI images block loopback URL literals.
// The direct-CDP launcher maps this reserved test host back to 127.0.0.1,
// preserving a real HTTP origin for modules/workers without bypassing app code.
const originHost = browserBackend === "chromium-cdp" ? "jmg.test" : "127.0.0.1";
const origin = `http://${originHost}:${address.port}`;
const browser = await launchBrowser();
const reports = [];
try {
for (const workload of workloads) {
for (let iteration = 0; iteration < repetitions; iteration++) {
const page = await browser.newPage();
page.setDefaultTimeout(timeoutMs);
const startedAt = performance.now();
const report = await runAppWorkload(page, origin, workload);
reports.push({ workloadName: workload.name, iteration, runnerWallMs: performance.now() - startedAt, ...report });
await page.close();
console.error(`[browser-e2e:${browserBackend}] ${workload.name} ${iteration + 1}/${repetitions}: ${report.status} ${Math.round(report.timing?.patchWallMs || 0)}ms`);
}
}
} finally {
await browser.close();
await new Promise((resolveClose) => server.close(resolveClose));
}
const strata = Object.fromEntries(workloads.map((workload) => {
const rows = reports.filter((report) => report.workloadName === workload.name);
const budgetMs = workload.kind === "cancel" ? 500 : 60_000;
return [workload.name, {
samples: rows.length,
pass: rows.length > 0 && rows.every((row) => row.status === "pass"),
patchBudgetMs: budgetMs,
patchP95Ms: rows.length ? percentile95(rows.map((row) => Number(row.timing?.patchWallMs || Infinity))) : Infinity,
maxProgressGapMs: rows.length ? Math.max(...rows.map((row) => Number(row.timing?.patchMaxProgressGapMs || 0))) : Infinity,
peakHeapBytes: rows.length ? Math.max(...rows.map((row) => Number(row.memory?.peakUsedJSHeapSize || 0))) : 0,
}];
}));
const expectedSamples = repetitions;
const releasePass = Object.values(strata).every(
(row) => row.samples === expectedSamples && row.pass && row.patchP95Ms < row.patchBudgetMs,
);
const summary = { profile, repetitions, browserBackend, releasePass, strata, reports };
console.log(JSON.stringify(summary, null, 2));
if (!releasePass) process.exitCode = 1;
}

View file

@ -2,27 +2,57 @@ import { performance } from "node:perf_hooks";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
const suites = [
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 requestedSuites = String(process.env.TEST_SUITES || "").split(",").map((value) => value.trim()).filter(Boolean);
const suites = requestedSuites.length ? requestedSuites : defaultSuites;
const concurrency = Math.max(1, Math.min(2, Number(process.env.TEST_CONCURRENCY) || 1));
const timeoutMs = 180_000;
// 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 started = performance.now();
console.error(`[test-all] start ${suite}`);
const child = spawn(process.execPath, [testFile, `--suite=${suite}`], {
const standaloneFile = suite === "additional-generation-unit"
? additionalUnitFile
: suite === "additional-generation-coverage-worker"
? additionalCoverageWorkerFile
: null;
const commandFile = standaloneFile || testFile;
const commandArgs = standaloneFile ? [commandFile] : [commandFile, `--suite=${suite}`];
const child = spawn(process.execPath, commandArgs, {
cwd,
stdio: ["ignore", "pipe", "pipe"],
});
@ -112,6 +142,9 @@ async function worker() {
const suite = queue.shift();
if (!suite) return;
completed.push(await runSuite(suite));
if (suiteCooldownMs > 0 && queue.length > 0) {
await new Promise((resolve) => setTimeout(resolve, suiteCooldownMs));
}
}
}
@ -126,17 +159,16 @@ const failures = completed.reduce(
const infrastructureFailure = completed.some(
(row) => row.status !== 0 || row.signal || row.timedOut || row.outputOverflow || row.infrastructureError,
);
const underThreeMinutes = wallSeconds < 180 && completed.every(
(row) => row.seconds < 180 && !row.timedOut,
);
const withinSuiteBudgets = completed.every((row) => !row.timedOut);
const result = {
underThreeMinutes,
withinSuiteBudgets,
wallSeconds,
concurrency,
suiteCooldownMs,
failures,
infrastructureFailure,
suites: completed,
};
console.log(JSON.stringify(result, null, 2));
if (!underThreeMinutes || infrastructureFailure || failures > 0) process.exitCode = 1;
if (!withinSuiteBudgets || infrastructureFailure || failures > 0) process.exitCode = 1;

View file

@ -1,4 +1,7 @@
import { generateMap, MAP_W, MAP_H, indexOf } from "../src/mapPipeline.js";
import { createWorldMap } from "../src/worldMap.js";
import { PATCH_MIN_HEIGHT, generatePatch } from "../src/mapPatch.js";
import { applyCommittedMirrorDelta, buildCommittedMirrorDelta, runPatchCandidateSearch } from "../src/mapPatchWorker.js";
import {
CUSTOM_NAME_LIST,
NAME_KANJI_POOLS,
@ -40,7 +43,7 @@ async function readLocalText(path) {
return readFile(new URL(path, import.meta.url), "utf8");
}
const [namesSource, mapGeneratorSource, mapOutputSource, mapTerrainSource, rendererSource, appSource, mapPipelineSource, mapAdminStageSource, mapPatchSource, worldMapSource, municipalSource, testSource] = await Promise.all([
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"),
@ -50,10 +53,17 @@ const [namesSource, mapGeneratorSource, mapOutputSource, mapTerrainSource, rende
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"),
]);
const derivePatchSeedStart = appSource.indexOf("function derivePatchSeed");
const derivePatchSeedEnd = derivePatchSeedStart >= 0 ? appSource.indexOf("\n}", derivePatchSeedStart) : -1;
const derivePatchSeedSource = derivePatchSeedStart >= 0 && derivePatchSeedEnd > derivePatchSeedStart
? appSource.slice(derivePatchSeedStart, derivePatchSeedEnd + 2)
: "";
function assert(condition, message) {
if (condition) logLines.push(`OK: ${message}`);
@ -63,6 +73,14 @@ function assert(condition, message) {
}
}
function arraysEqual(a, b) {
if (!a || !b || a.length !== b.length) return false;
for (let index = 0; index < a.length; index++) {
if (a[index] !== b[index] && !(Number.isNaN(a[index]) && Number.isNaN(b[index]))) return false;
}
return true;
}
function terrainBoundaryTargetForMetrics(map, i) {
const lu = map.landuse[i];
const urbanPenalty = Math.min(1, (lu === 3 ? 1.45 : lu === 2 ? 1.12 : lu === 4 ? 0.95 : lu === 7 ? 0.90 : lu === 8 ? 0.64 : lu === 5 || lu === 6 ? 0.48 : 0) + map.populationDensity[i] * 1.35);
@ -686,7 +704,7 @@ function transportConnectivityMetrics(map) {
try {
const size = MAP_W * MAP_H;
let capitalNameMaps = [];
let terrainSeedSummaries = [];
if (suiteEnabled("core")) {
const map = generateTestMap(12345);
const other = generateTestMap(54321);
@ -820,11 +838,216 @@ try {
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("normalizeGeneratedPointIds"), "patch generation normalizes generated point admin and prefecture ids");
assert(mapPatchSource.includes("prepareProductionTerrain") && mapPatchSource.includes("expansion-production-natural-overflow") && mapPatchSource.includes("regeneration-full-pipeline"), "patch modes retain the full human/admin pipeline while fast-selecting naturalized expansion terrain");
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");
assert(mapPatchSource.includes("resolvePatchMode") && mapPatchSource.includes("PATCH_MODE_EXPANSION") && mapPatchSource.includes("PATCH_MODE_REGENERATION"), "patch generation separates expansion and regeneration modes");
assert(mapPatchSource.includes("resolveWorldSeaLevel") && mapPatchSource.includes("buildTerrainBoundaryContract") && mapPatchSource.includes("applyTerrainBoundaryContract"), "patch terrain uses a shared world sea level and an explicit boundary contract");
assert(mapPipelineSource.includes("generateStableWorldTerrain") && mapPipelineSource.includes("stableTerrainSeed") && mapPipelineSource.includes("generateTerrainRect"), "expansion terrain is stable in absolute world coordinates");
assert(mapPatchSource.includes("candidateOriginX") && mapPatchSource.includes("world?.originX") && mapPatchSource.includes("canonicalWorldGrid"), "patch candidates use padding-invariant world coordinates and canonical tile windows");
assert(appSource.includes("activePatchOperation") && appSource.includes("selectionRevision") && appSource.includes("isPatchOperationCurrent"), "patch preview publication is guarded by immutable operation and selection generations");
assert(appSource.includes("fullGenerationBusy") && appSource.includes("state.patchBusy || state.fullGenerationBusy") && appSource.includes("cancelPatchGeneration"), "full and patch generation share one explicit busy/cancellation domain");
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(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");
assert(appSource.includes("stagePreviewRenderBundle") && appSource.includes("publishPreviewRenderBundle"), "preview rendering is staged offscreen before atomic state and canvas publication");
assert(mapPatchSource.includes("auditRepairAndReauditPatchSeam") && mapPatchSource.includes("PATCH_SEAM_INVARIANT_REASONS"), "seam repair is audit-driven and keeps invariant failures outside the repair path");
assert(mapPatchSource.includes("large-regeneration-final-quality") && mapPatchSource.includes("whole-selection-post-merge"), "tiled Regeneration applies one authoritative whole-selection quality audit");
assert(mapPatchSource.includes("minFinalAdminCenters") && mapPatchSource.includes("transportRequired")
&& mapPatchSource.includes("roadPaths > 0"), "final quality rejects Regeneration candidates that lose required administration or transport");
assert(mapPatchWorkerSource.includes("computePreviewDelta") && appSource.includes("previewPatchDelta(baseWorld, job.world"),
"preview raster and feature change auditing remains available for both immutable-reference and transactional-delta paths");
assert(appSource.includes("buildPatchCandidatePlan") && appSource.includes("consumedCandidateIds") && appSource.includes("nextVariant"), "UI continues Alternative batches without repeating content-rejected candidates");
const controllerBase = { fields: { marker: new Uint8Array([1]) } };
const controllerCalls = [];
const controllerProgress = [];
const controllerResult = runPatchCandidateSearch({
id: 1,
world: controllerBase,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: {
searchId: "controller-regression",
workerEpoch: 1,
committedRevision: 1,
candidatePlan: [{ variant: 5, seed: 105 }, { variant: 6, seed: 106 }, { variant: 7, seed: 107 }],
},
}, {
cloneWorld: (value) => structuredClone(value),
onProgress: (message) => controllerProgress.push(message.progress),
generateCandidate: (candidateWorld, rect, options) => {
controllerCalls.push({ variant: options.variant, baseline: candidateWorld.fields.marker[0] });
candidateWorld.fields.marker[0] = options.variant;
if (options.variant === 5) return { ok: false, code: "patch-quality-gate-failed", reason: "forced content rejection" };
return { ok: true, variant: options.variant, seed: options.seed, seamDiagnostics: { hardPass: true } };
},
});
assert(controllerResult.result?.ok === true && controllerResult.result?.actualVariant === 6, "content rejection automatically advances to the next complete candidate");
assert(controllerCalls.length === 2 && controllerCalls.every((call) => call.baseline === 1) && controllerBase.fields.marker[0] === 1, "each candidate starts from an isolated immutable committed baseline");
assert(controllerResult.result?.searchAttempts?.map((attempt) => attempt.status).join(",") === "rejected,success", "candidate search preserves an ordered rejection and success audit trail");
assert(controllerProgress.length > 0 && controllerProgress.every((event) => Number.isFinite(event.eventSeq) && event.phase && event.workUnitId), "worker progress carries ordered operation, phase, and work-unit identity");
assert(controllerProgress.filter((event) => event.boundedWork).every((event) => event.completed >= 0 && event.completed <= event.total), "bounded worker progress never exceeds its declared finite work total");
const invalidProgressResult = runPatchCandidateSearch({
id: 4,
world: controllerBase,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: { searchId: "controller-progress-invariant", candidatePlan: [{ variant: 12, seed: 112 }] },
}, {
cloneWorld: (value) => structuredClone(value),
generateCandidate: (candidateWorld, rect, options) => {
options.onProgress({ status: "step", key: "invalid-bounds", completed: 2, total: 1 });
return { ok: true, seamDiagnostics: { hardPass: true } };
},
});
assert(invalidProgressResult.ok === false && invalidProgressResult.code === "worker-progress-invariant", "invalid or runaway bounded progress stops the worker search as an invariant failure");
const invariantCalls = [];
const invariantResult = runPatchCandidateSearch({
id: 2,
world: controllerBase,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: {
searchId: "controller-invariant",
workerEpoch: 1,
committedRevision: 1,
candidatePlan: [{ variant: 8, seed: 108 }, { variant: 9, seed: 109 }],
},
}, {
cloneWorld: (value) => structuredClone(value),
generateCandidate: (candidateWorld, rect, options) => {
invariantCalls.push(options.variant);
return {
ok: false,
code: "patch-seam-gate-failed",
reason: "forced write escape",
seamDiagnostics: { hardPass: false, gateReasons: ["generated-footprint-write-escape"] },
};
},
});
assert(invariantResult.result?.searchStatus === "invariant-breach" && invariantCalls.length === 1, "write invariant failures stop the search without consuming another variant");
const cloneFailure = runPatchCandidateSearch({
id: 3,
world: controllerBase,
rect: { x0: 0, y0: 0, x1: 1, y1: 1 },
options: {},
search: { searchId: "controller-clone", candidatePlan: [{ variant: 10, seed: 110 }, { variant: 11, seed: 111 }] },
}, {
cloneWorld: () => { throw new Error("forced clone failure"); },
generateCandidate: () => { throw new Error("candidate must not start after clone failure"); },
});
assert(cloneFailure.result?.searchStatus === "infrastructure-error" && cloneFailure.result?.nextVariant === 10, "candidate clone failure preserves the current variant for infrastructure retry");
const deltaBase = {
width: 4, height: 2,
fields: { elevation: new Float32Array([0, 1, 2, 3, 4, 5, 6, 7]), adminId: new Int32Array(8) },
generatedMask: new Uint8Array(8), sourceMap: { villages: [{ x: 1, y: 1 }] }, patchGenerationSerial: 1,
};
const deltaTarget = structuredClone(deltaBase);
deltaTarget.fields.elevation[2] = 20;
deltaTarget.fields.adminId[7] = 9;
deltaTarget.generatedMask[6] = 1;
deltaTarget.sourceMap.villages.push({ x: 2, y: 2 });
deltaTarget.patchGenerationSerial = 2;
const committedDelta = buildCommittedMirrorDelta(deltaBase, deltaTarget);
const deltaApplied = applyCommittedMirrorDelta(structuredClone(deltaBase), committedDelta);
assert(
arraysEqual(deltaApplied.fields.elevation, deltaTarget.fields.elevation)
&& arraysEqual(deltaApplied.fields.adminId, deltaTarget.fields.adminId)
&& arraysEqual(deltaApplied.generatedMask, deltaTarget.generatedMask)
&& JSON.stringify(deltaApplied.sourceMap) === JSON.stringify(deltaTarget.sourceMap)
&& deltaApplied.patchGenerationSerial === 2,
"transactional Apply delta reproduces the accepted world fields, mask, metadata, and serial"
);
assert(mapPatchWorkerSource.includes("sourceMapDelta") && mapPatchWorkerSource.includes("metaDelta")
&& !mapPatchWorkerSource.includes("sourceMap: structuredClone(nextWorld.sourceMap"), "Apply ACK retains only changed source/world metadata instead of duplicating the complete map metadata");
assert(mapPatchWorkerSource.includes("buildExactArraySplice")
&& mapPatchWorkerSource.includes("arraySplices")
&& mapPatchWorkerSource.includes("isDensePlainArray")
&& committedWorldDeltaSource.includes("applyCommittedWorldDelta"),
"changed dense feature/path/history layers use exact splice deltas instead of transferring the complete layer");
assert(appSource.includes("consumeMetadata: true")
&& mapPatchWorkerSource.includes("pending.delta, { consumeMetadata: true }"),
"main preview publication and Worker Apply ACK consume their isolated metadata delta without cloning it a second time");
assert(mapPatchWorkerSource.includes("buildMainThreadTransferDelta")
&& !mapPatchWorkerSource.includes("const mainThreadDelta = structuredClone(delta)")
&& mapPatchWorkerSource.includes("{ fields: payload.worldDelta.fields, generatedMask: payload.worldDelta.generatedMask }"),
"Worker main-transfer preparation copies detachable raster rows without cloning or detaching retained metadata");
assert(mapPatchWorkerSource.includes("const exactDeltas = { sourceMapDelta: rawSourceMapDelta, metaDelta: rawMetaDelta }")
&& !mapPatchWorkerSource.includes("structuredClone({ sourceMapDelta: rawSourceMapDelta, metaDelta: rawMetaDelta })"),
"transaction delta owns completed candidate metadata directly instead of cloning the graph before rollback");
assert(mapPatchWorkerSource.includes("transactional: true") && mapPatchWorkerSource.includes("buildCommittedMirrorDeltaFromTransaction")
&& appSource.includes("materializeCommittedWorldDeltaCooperative(world, event.data.worldDelta"), "production previews mutate the Worker mirror transactionally and materialize only the accepted delta on the main thread");
assert(appSource.includes("materializeCommittedWorldDelta")
&& !appSource.includes("previewWorld = structuredClone(world)"),
"main preview creation uses copy-on-write changed fields instead of cloning every committed raster and metadata layer");
assert(appSource.includes("Accepted preview hash mismatch") && appSource.includes("hashCommittedWorldAsync")
&& mapPatchWorkerSource.includes("acceptedWorldHash"),
"main-thread preview publication cooperatively rejects any transaction delta that does not reproduce the Worker-completed world hash");
assert(mapPatchWorkerSource.includes("changeTracker.mask") && mapPatchWorkerSource.includes("successDelta?.previewDelta"),
"transaction delta construction also produces preview statistics so the main thread does not repeat the field comparison");
assert(mapPatchWorkerSource.includes("const scanLocalRect = localEntry && rect")
&& mapPatchWorkerSource.includes("const scanStart = scanLocalRect"),
"transaction delta compares local fields only inside their captured mutation rectangle");
assert(mapPatchWorkerSource.includes("const transferRoot = payload.worldDelta")
&& mapPatchWorkerSource.includes("{ fields: payload.worldDelta.fields, generatedMask: payload.worldDelta.generatedMask }")
&& mapPatchWorkerSource.includes(": (payload.world || null)"),
"result transfer discovery scans only the transferable raster delta/full-world root instead of the complete diagnostic graph");
assert(!worldMapSource.includes("invalidatedRects") && !mapPatchSource.includes("addInvalidatedRect")
&& !worldMapSource.includes("humanPatchHistory") && !mapPatchSource.includes("humanPatchHistory"),
"unused invalidation and patch-history arrays are absent from world state, transactions, padding shifts, and Worker payloads");
assert(mapPatchSource.includes("PATCH_MUTABLE_SOURCE_KEYS")
&& mapPatchSource.includes("PATCH_MUTABLE_SOURCE_KEYS.has(key)")
&& mapPatchSource.includes("out[key] = PATCH_MUTABLE_SOURCE_KEYS.has(key)"),
"transaction snapshots recursively clone only patch-mutable sourceMap roots and share read-only production metadata");
assert(mapPatchWorkerSource.includes("isolateSourceMap: true")
&& mapPatchSource.includes("sourceMapIsolated: isolateSourceMap")
&& mapPatchSource.includes("world.sourceMap = snapshot.sourceMapRef || {}"),
"Worker candidates mutate an isolated sourceMap and rollback by reference without a second metadata clone");
assert(mapPatchSource.includes("generatedRects: lightweight ? null : (world.generatedRects || [])")
&& mapPatchSource.includes("lastPatchResult: lightweight ? null : (world.lastPatchResult ?? null)")
&& !mapPatchSource.includes("cloneTransactionValue(world.generatedRects || [])")
&& !mapPatchSource.includes("cloneTransactionValue(world.lastPatchResult ?? null)"),
"transaction rollback retains immutable history and prior diagnostics by reference instead of cloning their debug graphs");
assert(mapPatchSource.includes("baselineSourceMap") && mapPatchSource.includes("snapshotPoint = baselineSourceMap")
&& mapPatchSource.includes("capturePrefectureIdentitySnapshot(sourceMap, strictMetadataSnapshot)"),
"strict Regeneration metadata reuses the immutable transaction baseline across outside-point and identity snapshots");
assert(mapPatchSource.includes("rects.patchMode === PATCH_MODE_EXPANSION")
&& mapPatchSource.includes("protectedIndexLookup?.fill(-1)"),
"Regeneration strict snapshots do not allocate the Expansion-only random lookup table");
assert(mapPatchSource.includes("_strictBaselineTransactionSnapshot: transaction")
&& mapPatchSource.includes("transactionSnapshot?.fields?.has(name)"),
"large internal tiles reuse the outer transaction's field before-images instead of cloning strict fields per tile");
assert(mapPatchSource.includes("PATCH_TRANSACTION_READ_ONLY_FIELDS")
&& mapPatchSource.includes("PATCH_TRANSACTION_READ_ONLY_FIELDS.has(name)"),
"transaction and strict snapshots omit the read-only flowTo field instead of copying unused rollback values");
assert(appSource.includes("resolvedPatchMode: operation.resolvedPatchMode")
&& mapPatchWorkerSource.includes("copyGeneratedMask:")
&& mapPatchWorkerSource.includes("resolvedPatchMode || \"\").toLowerCase() !== \"regeneration\"")
&& mapPatchSource.includes("generatedMaskRef"),
"Regeneration transactions retain generatedMask by reference while Expansion keeps an exact writable before-image");
assert(mapPatchSource.includes("synchronizePatchMunicipalityField") && mapPatchSource.includes("municipalityWriteRects: aggregateSourceRects"),
"Regeneration administrative coherence avoids a whole-world municipality write followed by whole-world strict restoration");
assert(mapPatchSource.includes("municipalityWriteRects: rects,")
&& !mapPatchSource.includes("snapshot.globalFields.set(\"municipalityId\"")
&& !mapPatchSource.includes("new municipality.constructor(municipality)")
&& !mapPatchSource.includes("globalFields:"),
"Expansion and Regeneration scope municipality writes to patch alpha and omit the full-world rollback copy");
assert(mapPatchSource.includes("bestFallbackCellByPrefecture") && mapPatchSource.includes("previous || score > previous.score"),
"prefecture capital fallback collects best cells in one world pass instead of rescanning the world per missing prefecture");
assert(mapPatchSource.includes("_tileCoreWidth: MAP_W") && mapPatchSource.includes("_tileCoreHeight: MAP_H")
&& appSource.includes("buildLargeExpansionTiles(rect, world, regeneration ? {")
&& appSource.includes("_tileCoreWidth: MAP_W") && appSource.includes("_tileCoreHeight: MAP_H"),
"large Regeneration plans full-size production cores and the UI derives the same tile count from the production tiler");
assert(!mapPatchWorkerSource.includes("stableLayerText") && !appSource.includes("stableLayerText"), "preview feature comparison does not allocate full JSON strings");
assert(rendererSource.includes("MAX_BASE_CACHE_IMAGES = 1") && rendererSource.includes("MAX_OVERLAY_CACHE_IMAGES = 1")
&& !appSource.includes("snapshotVisibleCanvas"), "preview raster caches and publication rollback do not retain obsolete full canvases");
assert(worldMapSource.includes("initialQualityReference") && mapPatchSource.includes("world?.initialQualityReference"), "quality reference is fixed at initial generation instead of growing with patch history");
assert(worldMapSource.includes("generatedMask") && mapPatchSource.includes("addGeneratedFootprintToMask"), "generated coverage uses a world-sized mask rather than scanning all patch history per cell");
assert(appSource.includes("featureLayersChanged") && appSource.includes("transportReachRect") && appSource.includes("fieldNames"), "preview identical diagnostics cover all raster fields and feature/path layers in the affected range");
assert(mapPatchSource.includes("_deferInternalSeamDiagnostics") && mapPatchSource.includes("_coverageDistanceBaseline"), "large tiles reuse coverage distances and defer internal seam diagnostics to the whole selection");
assert(worldMapSource.includes("MAX_WORLD_WIDTH") && worldMapSource.includes("MAX_WORLD_HEIGHT"), "backing-world padding has an explicit upper bound");
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");
@ -1064,18 +1287,26 @@ try {
}
if (suiteEnabled("terrain-name")) {
const semeMap = generateTestMap(8363712);
const semeAdmin = (semeMap.adminCenters || []).find((center) => center.canonicalSettlementName);
assert(!semeAdmin || String(semeAdmin.name).replace(/[市町村]$/u, "") === String(semeAdmin.canonicalSettlementName).replace(/[市町村]$/u, ""),
"seed 8363712: municipality label preserves the canonical settlement root");
}
if (suiteEnabled("terrain")) {
const blockedCapitalName = "\u52A0\u8302";
capitalNameMaps = [114514, 12345, 54321, 777, 999].map((seedValue) => generateTestMap(seedValue));
const capitalNames = capitalNameMaps.map((seeded) => seeded.prefecturalCapital?.name).filter(Boolean);
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");
const semeMap = generateTestMap(8363712);
const semeAdmin = (semeMap.adminCenters || []).find((center) => center.canonicalSettlementName);
assert(!semeAdmin || String(semeAdmin.name).replace(/[市町村]$/u, "") === String(semeAdmin.canonicalSettlementName).replace(/[市町村]$/u, ""), "seed 8363712: municipality label preserves the canonical settlement root");
for (const [n, seeded] of capitalNameMaps.entries()) {
const seedValue = [114514, 12345, 54321, 777, 999][n];
const terrainSeeds = [114514, 12345, 54321, 777, 999];
const capitalNames = [];
terrainSeedSummaries = [];
for (const seedValue of terrainSeeds) {
// Evaluate and release each complete map before generating the next seed.
// Retaining five full raster worlds at once made this validation shard
// memory-pressure dependent without increasing its coverage.
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 });
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`);
@ -1113,6 +1344,8 @@ 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`);
}
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-")) {
@ -1125,9 +1358,7 @@ try {
}
if (suiteEnabled("terrain")) {
const byDeposition = capitalNameMaps
.map((seeded) => ({ deposition: seeded.terrainTemplate.deposition, lowlandRatio: terrainCoreMetrics(seeded).lowlandRatio }))
.sort((a, b) => a.deposition - b.deposition);
const byDeposition = terrainSeedSummaries.slice().sort((a, b) => a.deposition - b.deposition);
assert(byDeposition[byDeposition.length - 1].lowlandRatio >= byDeposition[0].lowlandRatio * 0.72, "higher-deposition templates generally preserve or expand lowland area");
const originalCustomNames = [...CUSTOM_NAME_LIST];
CUSTOM_NAME_LIST.length = 0;
@ -1187,8 +1418,106 @@ try {
}
}
if (suiteEnabled("patch")) {
const initial = generateTestMap(DETERMINISM_SEED);
const world = createWorldMap(initial);
assert(!world.sourceMap.sea && !world.sourceMap.elevation && world.initialQualityReference?.landCells > 0, "world metadata omits duplicate fixed-map raster fields while retaining the immutable quality reference");
const rect = {
x0: world.originX + 72,
y0: world.originY + 58,
x1: world.originX + 132,
y1: world.originY + 118,
};
const outsideIndex = (world.originY + 12) * world.width + world.originX + 12;
const outsideAdminId = world.fields.adminId?.[outsideIndex];
const patchStartedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
const patch = generatePatch(world, rect, {
patchMode: "regeneration",
terrainType: "auto",
seed: 0x51a7c3d3,
variant: 1,
maxQualityRetries: 0,
qualityTerrainAttempts: 1,
includeSeamVisualization: true,
});
const patchElapsedMs = (typeof performance !== "undefined" ? performance.now() : Date.now()) - patchStartedAt;
assert(patch?.ok === true, "standard patch suite executes generatePatch and returns a preview candidate");
assert(patch?.variant === 1 && patch?.seed === 0x51a7c3d3, "executed patch preserves the explicitly requested variant and seed");
assert(patchElapsedMs < 30000, `small production patch completes within the 30 s budget (${Math.round(patchElapsedMs)} ms)`);
assert(patch?.seamDiagnostics?.hardPass === true && (patch?.seamDiagnostics?.prefectureSeamBreakEdges || 0) === 0, "small Regeneration repairs only the real ownership seam and passes the unchanged hard seam gate");
const restoredPrefectureCells = patch?.seamDiagnostics?.administrativeSeamRepair?.prefectureCellsRestored || 0;
const regeneratedArea = Math.max(1, (rect.x1 - rect.x0) * (rect.y1 - rect.y0));
// Administrative seam repair is allowed only as a narrow boundary repair.
// Use an area-relative cap rather than an obsolete fixture-specific count:
// this still catches accidental interior rewrites while allowing a handful
// of independent broken ownership edges to be restored deterministically.
const localizedAdminRepairCap = Math.max(12, Math.ceil(regeneratedArea * 0.005));
assert(restoredPrefectureCells <= localizedAdminRepairCap,
`administrative seam repair remains localized instead of rewriting the regenerated interior (restored=${restoredPrefectureCells}, cap=${localizedAdminRepairCap})`);
assert((patch?.candidateUnmappedActiveCells || 0) === 0, "executed patch maps every active write cell into the production candidate");
assert(world.fields.adminId?.[outsideIndex] === outsideAdminId,
`strict Regeneration preserves canonical administrative fields outside the selection (before=${outsideAdminId}, after=${world.fields.adminId?.[outsideIndex]})`);
const serializedRects = JSON.stringify(patch?.rects || {});
const serializedRectKeys = Object.getOwnPropertyNames(JSON.parse(serializedRects));
const serializedWorkCacheKeys = serializedRectKeys.filter((key) => key === "patchAlphaCache" || key === "patchSourceIndexCache");
if (serializedWorkCacheKeys.length > 0) {
failed += 1;
logLines.push(`NG: worker-only patch caches are absent from the serialized result payload (cacheKeys=${serializedWorkCacheKeys.join(",")})`);
} else {
logLines.push("OK: worker-only patch caches are absent from the serialized result payload");
}
const expectedPopulation = [...(world.sourceMap.modernCities || []), ...(world.sourceMap.satelliteCities || [])]
.reduce((sum, city) => sum + (Number(city?.population) || 0), 0);
assert(world.sourceMap.totalPopulation === expectedPopulation, "patch application refreshes total population metadata");
const generatedPoint = [
...(world.sourceMap.modernCities || []), ...(world.sourceMap.satelliteCities || []),
...(world.sourceMap.villages || []), ...(world.sourceMap.markets || []),
].find((point) => point?.patchGenerated && Number.isFinite(point.regionId));
if (generatedPoint && world.fields.regionId) {
const wx = Math.round((generatedPoint.worldX ?? generatedPoint.x + world.originX));
const wy = Math.round((generatedPoint.worldY ?? generatedPoint.y + world.originY));
const wi = wy * world.width + wx;
assert(generatedPoint.regionId === world.fields.regionId[wi], "patch-generated point regionId matches the persisted world raster regionId");
} else if (generatedPoint) {
// regionId remains point metadata in the current final world schema; the
// internal generation raster is intentionally not persisted by mapOutput.
assert(Number.isFinite(generatedPoint.regionId), "patch-generated point keeps a finite regionId when no persisted regionId raster exists");
} else {
assert(true, "executed patch produced no region-tagged point requiring a regionId check");
}
}
if (suiteEnabled("patch-large")) {
const initial = generateTestMap(DETERMINISM_SEED);
const world = createWorldMap(initial);
const rect = {
x0: world.originX,
y0: world.originY + 64,
x1: world.originX + MAP_W + 1,
y1: world.originY + 64 + PATCH_MIN_HEIGHT,
};
const serialBefore = world.patchGenerationSerial || 0;
const largeStartedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
const large = generatePatch(world, rect, {
patchMode: "regeneration",
terrainType: "auto",
seed: 0x6d2b79f5,
variant: 1,
maxQualityRetries: 0,
qualityTerrainAttempts: 1,
includeSeamVisualization: true,
});
const largeElapsedMs = (typeof performance !== "undefined" ? performance.now() : Date.now()) - largeStartedAt;
assert(large?.ok === true && large?.tiledRegeneration === true && Number(large?.tileCount || 0) >= 2, "large Regeneration must publish a complete canonical-tile preview; rollback alone is not a passing result");
assert(largeElapsedMs < 60000, `large production Regeneration completes within the 60 s budget (${Math.round(largeElapsedMs)} ms)`);
assert(large?.seamDiagnostics?.hardPass === true, "published large Regeneration passes the whole-selection seam gate");
assert((large?.candidateUnmappedActiveCells || 0) === 0, "large Regeneration maps every active write cell across all tiles");
assert((world.patchGenerationSerial || 0) === serialBefore + 1, "large Regeneration records one logical operation rather than internal tile history");
}
const elapsedMs = (typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - TEST_STARTED_AT;
assert(elapsedMs < 180000, `test shard ${TEST_SUITE} completes under three minutes`);
const shardBudgetMs = Math.max(180000, fullMapGenerations * 75000);
assert(elapsedMs < shardBudgetMs, `test shard ${TEST_SUITE} completes within its complete-generation workload budget (${Math.round(elapsedMs)} / ${shardBudgetMs} ms)`);
logLines.push(`INFO: suite=${TEST_SUITE}; fullMapGenerations=${fullMapGenerations}; elapsedMs=${Math.round(elapsedMs)}`);
result.className = failed === 0 ? "ok" : "ng";
result.textContent = `${failed === 0 ? "All tests passed." : `${failed} tests failed.`}\n\n${logLines.join("\n")}`;