Files
deepgrain-foundry/test/t_lakes.js
T
44r0n7andClaude Opus 5.5 daeb3b2238 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
2026-10-29 20:55:52 -04:00

83 lines
5.3 KiB
JavaScript

// 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; }
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).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, '3e97601ea08027e7f21fa3d52ec300fb7e13c630a1ddad0a7be6ca7ab4b07592'],
[42, 'ecf71e4309c0b19654fbc17dbb0b5717023842d331f4f12c010d3312e9f9383f'],
[424242, '235a33a7c85118d05cc9f0508acaa266b61d5f9f100703c0a095d760fe80066d'],
[2, null],
[1, null],
// These lakes once ran over their lower rim up to the trade dock.
[5, null],
[6, null],
]) {
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 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)`));
// 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 }));
}
// 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();