Rebuild lake generation: bowls in valleys filled to their spill level (t96)

Lakes no longer pour water to a guessed lip and repair it afterwards. Each lake lowers the land
in the nearest valley with a smoothstep bowl, and water fills it to its spill level (the lower of
the highest ground on each side, Priority-Flood in one dimension), so it is flat and contained by
construction. Clay beds, no caves or loose pockets close under lakes, trees kept off the shore.
Removes the levelling pass, the lower-rim lip and the bank-slope cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cj2RPXdPykKMEz5BXXczsj
This commit is contained in:
2026-10-29 20:55:52 -04:00
co-authored by Claude Opus 5.5
parent e3e01a84b1
commit daeb3b2238
9 changed files with 165 additions and 206 deletions
+55 -53
View File
@@ -1,80 +1,82 @@
// Generated lakes start flat, conserve water, and have dry tree bases.
const { load, suite, ticks, count, rect } = require('./harness');
// Generated lakes sit in a smooth bowl filled to its spill level: flat, contained, clay-bedded,
// stable, with natural banks, dry tree bases and the dock clear.
const { load, suite, ticks } = require('./harness');
const { createHash } = require('node:crypto');
const { run } = load();
const { check, done } = suite();
// Surface pools with their depth, water count, and the ground top of the dry column on each side.
function surfacePools() {
return run(`(() => {
const top = x => { let y = 0; while (y < H && (mat[y * W + x] === EMPTY || mat[y * W + x] === WATER)) y++; return y; };
const pools = []; let pool = null;
for (let x = 0; x < W; x++) {
let y = 0; while (y < H && mat[y * W + x] === EMPTY) y++;
if (mat[y * W + x] !== WATER) { pool = null; continue; }
if (!pool) { pool = { x0: x, x1: x, width: 0, min: y, max: y }; pools.push(pool); }
let b = y; while (b < H && mat[b * W + x] === WATER) b++;
if (!pool) { pool = { x0: x, x1: x, width: 0, min: y, max: y, deep: 0, water: 0, clay: true }; pools.push(pool); }
pool.x1 = x; pool.width++; pool.min = Math.min(pool.min, y); pool.max = Math.max(pool.max, y);
pool.deep = Math.max(pool.deep, b - y); pool.water += b - y; pool.clay = pool.clay && mat[b * W + x] === CLAY;
}
return pools.filter(p => p.width > 20);
return pools.filter(p => p.width > 20).map(p => Object.assign(p, { left: top(p.x0 - 1), right: top(p.x1 + 1) }));
})()`);
}
// Largest ground step within 40 columns of each pool, away from the dock's foundation.
function bankStep(pools) {
return run(`(() => { const d = machines.find(m => m && m.type === 'dock');
const top = x => { let y = 0; while (y < H && (mat[y * W + x] === EMPTY || mat[y * W + x] === WATER)) y++; return y; };
let worst = 0;
for (const p of ${JSON.stringify(pools)}) for (let x = Math.max(1, p.x0 - 40); x < Math.min(W - 1, p.x1 + 40); x++)
if (x < d.x - 2 || x > d.x + 26) worst = Math.max(worst, Math.abs(top(x + 1) - top(x)));
return worst; })()`);
}
function lakeChecks(seed, pools) {
check(`seed ${seed} has a real lake`, pools.some(p => p.width >= 120) && pools.every(p => p.deep >= 12), JSON.stringify(pools));
check(`seed ${seed} lakes are exactly flat`, pools.every(p => p.max === p.min), JSON.stringify(pools));
// Contained: the dry column on each side reaches at least the water's surface, so nothing can spill.
check(`seed ${seed} lakes are held by their banks`, pools.every(p => p.left <= p.min && p.right <= p.min), JSON.stringify(pools));
check(`seed ${seed} lake beds are clay`, pools.every(p => p.clay));
// Banks follow the land: round 3 left a 15-grain cliff, round 4 a ruler-straight ramp.
check(`seed ${seed} lake banks have no cliff`, bankStep(pools) <= 3, bankStep(pools));
check(`seed ${seed} keeps its lakes clear of the trade dock`, run(`(() => { const d = machines.find(m => m && m.type === 'dock');
return ${JSON.stringify(pools)}.every(p => p.x1 < d.x - 8 || p.x0 > d.x + 24 + 8); })()`), JSON.stringify(pools));
check(`seed ${seed} has no submerged tree base`, run('treeBases.every(i => !treeHeld(i) || mat[i] !== WATER)'));
check(`seed ${seed} retains dry trees`, run('treeBases.filter(i => treeHeld(i)).length > 20'));
}
// Spill levels on a hand-made surface: each column fills to the lower of its two highest sides.
run('alloc(16, 64); surfY.set([20, 30, 34, 32, 26, 30, 36, 33, 24, 40, 40, 22, 25, 25, 25, 25]); globalThis.levels = new Int16Array(16);');
// The highest ground right of x=1 is 22 (x=11), lower than the 20 on the left, so x=1..11 fill
// to 22; past x=11 nothing on the right rises above 25. The deepest water is 18 (x=9, 10).
check('spill levels fill each basin to its lower rim', run('spillLevels(0, 15, levels) === 18 && [20,22,22,22,22,22,22,22,22,22,22,22,25,25,25,25].every((v, x) => levels[x] === v)'), run('Array.from(levels)'));
for (const [seed, expected] of [
[7, '5e230523dc4dda212e7c67ceb7f3b496ff201d826366e1d7c379efd1351d1956'],
[42, 'cc3d150a91caac6ab88dfefbd45d85c5bc5c38fc77d7913b9de157647f4b9d7b'],
[424242, '1339c90f4f7f1d245c2e21aa4b8d23b21c199b8251392256f6de5ed704e2b396'],
[7, '3e97601ea08027e7f21fa3d52ec300fb7e13c630a1ddad0a7be6ca7ab4b07592'],
[42, 'ecf71e4309c0b19654fbc17dbb0b5717023842d331f4f12c010d3312e9f9383f'],
[424242, '235a33a7c85118d05cc9f0508acaa266b61d5f9f100703c0a095d760fe80066d'],
[2, null],
[1, null],
// Before t96 round 3 these lakes ran over their lower rim up to the trade dock.
// These lakes once ran over their lower rim up to the trade dock.
[5, null],
[6, null],
]) {
run(`generateWorld(${seed}, 2048, 1024, (f, label) => {
if (f === 0.99 && label === 'Finishing') globalThis.waterBeforeLevel = mat.reduce((n, m) => n + (m === WATER), 0);
}); weather.next = 1e15;`);
run(`generateWorld(${seed}, 2048, 1024); weather.next = 1e15;`);
const hash = createHash('sha256');
for (const name of ['mat', 'temp']) hash.update(Buffer.from(run(name).buffer));
const actual = hash.digest('hex');
if (expected) check(`seed ${seed} matches levelled generation hash`, actual === expected, actual);
check(`seed ${seed} conserves water through levelling`, run('mat.reduce((n, m) => n + (m === WATER), 0) === waterBeforeLevel'));
const initial = surfacePools();
check(`seed ${seed} starts with flat surface pools`, initial.length > 0 && initial.every(p => p.max - p.min <= 1), JSON.stringify(initial));
// Lakes dug to their lower rim once left a cliff on the high side (seed 766733's 15-grain step).
check(`seed ${seed} lake banks slope with no cliff`, run(`(() => { const d = machines.find(m => m && m.type === 'dock');
const top = x => { let y = 0; while (y < H && (mat[y * W + x] === EMPTY || mat[y * W + x] === WATER)) y++; return y; };
return ${JSON.stringify(initial)}.every(p => { for (let x = Math.max(1, p.x0 - 40); x < Math.min(W - 1, p.x1 + 40); x++)
if (x < d.x - 2 || x > d.x + 26) { if (Math.abs(top(x + 1) - top(x)) > 3) return false; } return true; }); })()`), JSON.stringify(initial));
check(`seed ${seed} keeps its lakes clear of the trade dock`, run(`(() => { const d = machines.find(m => m && m.type === 'dock');
return ${JSON.stringify(initial)}.every(p => p.x1 < d.x - 8 || p.x0 > d.x + 24 + 8); })()`), JSON.stringify(initial));
if (expected) check(`seed ${seed} matches its generation hash`, actual === expected, actual);
const pools = surfacePools();
lakeChecks(seed, pools);
check(`seed ${seed} restores pinned generation switches`, run('LSTEP === 8 && LFILM === 4 && SORTP === 0.06 && CORNER_SEAL && POWDER_FAN'));
check(`seed ${seed} starts every water area awake`, run(`mat.every((m, i) => m !== WATER || awake[((i / W) >> 5) * CW + ((i % W) >> 5)] > 0)`));
check(`seed ${seed} has no submerged tree base`, run('treeBases.every(i => !treeHeld(i) || mat[i] !== WATER)'));
check(`seed ${seed} retains dry trees`, run('treeBases.filter(i => treeHeld(i)).length > 20'));
ticks(run, 600);
const settled = surfacePools();
check(`seed ${seed} stays flat after 600 ticks`, settled.length > 0 && settled.every(p => p.max - p.min <= 1), JSON.stringify(settled));
for (const lake of initial) check(`seed ${seed} original lake retains dry roots`, run(`treeBases.every(i => i % W < ${lake.x0} || i % W > ${lake.x1} || !treeHeld(i) || mat[i] !== WATER)`));
// Stable: no leak into a cave and no slump; the surface and the water in each lake stay put.
ticks(run, 3000);
const later = surfacePools();
check(`seed ${seed} lakes keep their surface and water for 3000 ticks`, later.length === pools.length &&
later.every((p, k) => p.x0 === pools[k].x0 && p.x1 === pools[k].x1 && p.min === pools[k].min && p.max === pools[k].max && p.water === pools[k].water),
JSON.stringify({ pools, later }));
}
// A low rim with a much deeper empty basin beyond it must not steal the water.
for (const wall of ['STONE', 'SAND']) {
run('alloc(64, 64); mat.fill(STONE);');
rect(run, 5, 10, 24, 29, run('EMPTY'));
rect(run, 26, 10, 55, 45, run('EMPTY'));
rect(run, 25, 22, 25, 45, run(wall));
rect(run, 25, 10, 25, 21, run('EMPTY'));
rect(run, 5, 20, 14, 29, run('WATER'));
rect(run, 15, 27, 15, 29, run('WATER'));
run('globalThis.beforeLake = mat.slice(); levelWorldWater();');
check(`${wall} rim holds all 103 grains`, count(run, run('WATER')) === 103 && count(run, run('WATER'), 26, 0) === 0);
check(`${wall} basin fills from bottom upward`, count(run, run('WATER'), 5, 25, 24, 29) === 100);
check(`${wall} partial top row fills from the middle outward`, run('[13,14,15].every(x => mat[24 * W + x] === WATER)') && count(run, run('WATER'), 0, 0, 63, 24) === 3);
check(`${wall} levelling changes only water and air`, run('mat.every((m,i) => m === beforeLake[i] || ((m === EMPTY || m === WATER) && (beforeLake[i] === EMPTY || beforeLake[i] === WATER)))'));
}
// Diagonally touching small bodies remain separate; exactly 50 grains is eligible.
run('alloc(64,64); mat.fill(STONE);');
rect(run, 5, 20, 11, 26, run('WATER'));
rect(run, 12, 27, 18, 33, run('WATER'));
rect(run, 5, 10, 11, 19, run('EMPTY'));
rect(run, 30, 10, 49, 29, run('EMPTY'));
rect(run, 30, 20, 34, 29, run('WATER'));
run('globalThis.smallBefore = mat.slice(); levelWorldWater();');
check('diagonally touching 49-grain pools are left intact', run('mat.every((m,i) => i % W >= 30 || m === smallBefore[i])'));
check('50-grain body levels and conserves volume', count(run, run('WATER'), 30, 28, 49, 29) === 40 && count(run, run('WATER'), 30, 0) === 50);
// One of the owner's Huge seeds, whose round-3/4 banks showed the cliff and the ramp.
run('generateWorld(766733, 6144, 2048); weather.next = 1e15;');
lakeChecks(766733, surfacePools());
done();