Level lakes at the end of world generation, keeping their water (t96)

Volume-preserving levelling pass for water bodies of 50+ cells; the dock foundation is placed
first so it cannot erase levelled water. Seed hashes re-baselined (water and air only).
Started by Codex, finished and reviewed by Claude after Codex hit its usage limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 19:28:23 -04:00
co-authored by Claude Opus 5.5
parent 75d4e8ee4e
commit ae78756fa2
10 changed files with 227 additions and 50 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ def main():
if browser:
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
# one at a time: two headless browsers at once can starve each other and flake
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_vacuum_collapse.py', 't_blow_collapse.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_vacuum_collapse.py', 't_blow_collapse.py', 't_lakes.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
results.append(run_py(name))
bad = 0
for name, code, out, secs in results:
+4 -3
View File
@@ -4,10 +4,11 @@ const { createHash } = require('node:crypto');
const { run } = load();
const { check, done } = suite();
// t96 re-baseline: only the generation lake-levelling pass changes these material bytes.
for (const [seed, expected] of [
[777, 'b6f7b493e93a59e38ee57d33f24690daa87e7be0f4b356b32225a3a6f1a9d001'],
[77, '5e08c24603686c8617dd1156b10612b91a05fd9ee8b3648dfc4ee32fc008383a'],
[4242, 'f7ca75d19eba9eae4894b21b95fe1b683bd54e1f65a415dcc296f3c23b5d7e05'],
[777, '1fac6b4b6420f328d7de87c0813e328303271e7605d85ce541e108361aff0d90'],
[77, 'de3ec38547585486924f53abb82cb3c0f76e0bd24b75873ddf96599b5886b52a'],
[4242, '57cd89b5518c3e342fc1b91243cd068dc2e38a2d42de84a1c2ee1cbf936c2f1a'],
]) {
run(`generateWorld(${seed}, 512, 384)`);
check(`seed ${seed} restores corner sealing`,run('CORNER_SEAL')===true);
+45 -26
View File
@@ -1,9 +1,9 @@
// Generated lakes wake without a tool, settle on their own, and have dry tree bases.
const { load, suite, ticks } = require('./harness');
// Generated lakes start flat, conserve water, and have dry tree bases.
const { load, suite, ticks, count, rect } = require('./harness');
const { createHash } = require('node:crypto');
const { run } = load();
const { check, done } = suite();
const water = run('WATER');
function surfacePools() {
return run(`(() => {
const pools = []; let pool = null;
@@ -17,35 +17,54 @@ function surfacePools() {
})()`);
}
for (const [seed, expected] of [
[7, '865ac320f743ff5ed573acab3333773b5ebc2e10f15600438dd15f1767acb72e'],
[42, '94930e108f7bd948c6f3dbe8b15b2950eed695cb3d57376d05a8ad781c2015b7'],
[424242, '8b8a4b44c2cc98d3ab86515eb55f38ce7cb3d6a283e29e0ed758af937ea8e8c8'],
[7, '669a4ee87496be27473d2987d9a8935be1a19b29a8f2433d306f32a1616f3bca'],
[42, 'efeadd2bff0ce3eaf81e5a08a998ce767e9667e3b27b8623e928f202669f640e'],
[424242, '24b5e6ccc4e7d5eb9d54c3ff0fa5dfe84185b83e75b07261ac007b4f920f0640'],
[2, null],
[1, null],
]) {
run(`generateWorld(${seed}, 2048, 1024); weather.next = 1e15;`);
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;`);
const hash = createHash('sha256');
for (const name of ['mat', 'temp']) hash.update(Buffer.from(run(name).buffer));
check(`seed ${seed} preserves generated material and temperature bytes`, hash.digest('hex') === expected);
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));
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'));
// Seed 7 reproduces the owner's long slope. No drill, forced wakes, or terrain edits.
if (seed === 7) {
run('globalThis.lakeSave = serializeWorld();');
for (const state of ['fresh', 'reloaded']) {
if (state === 'reloaded') run('deserializeWorld(lakeSave.meta, lakeSave.buffers); weather.next = 1e15;');
check(`${state} world starts with dry tree bases`, run('treeBases.every(i => !treeHeld(i) || mat[i] !== WATER)'));
const before = run('mat.slice(270 * W, 340 * W)');
const wakeTicks = state === 'fresh' ? 1 : 171;
ticks(run, wakeTicks);
check(`${state} lake moves within its startup wake window`, run('mat.subarray(270 * W, 340 * W)').some((m, i) => m !== before[i] && (m === water || before[i] === water)));
ticks(run, 6000 - wakeTicks);
const pools = surfacePools();
check(`${state} lake levels within two grains without disturbance`, pools.length > 0 && pools.every(p => p.max - p.min <= 2), JSON.stringify(pools));
// Reload can also thaw mountain snow into new puddles; check the original lake here.
const lake = pools.reduce((a, b) => a.width > b.width ? a : b);
check(`${state} original lake has no submerged tree base after settling`, run(`treeBases.every(i => i % W < ${lake.x0} || i % W > ${lake.x1} || !treeHeld(i) || mat[i] !== WATER)`));
}
}
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)`));
}
// 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);
done();
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""t96: flat generated lakes, fresh and reloaded, in the real pixel frame loop."""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / 'tools'))
from use_venv import ensure_playwright
ensure_playwright()
from browser import launch
errors = []
with launch(viewport={'width':1280,'height':760}) as (_, pg, url):
pg.on('pageerror', lambda e: errors.append(str(e)))
pg.goto(url)
pg.evaluate('CAPS_CHECK')
pg.evaluate('''() => { SET.workerCap=2; SET.fog=false; SET.look='pixel'; SET.autosave=0; SET.cameraLead=false;
const enter=enterGame; enterGame=function(){enter();GAME.paused=true;}; }''')
pg.evaluate('startNewWorld(7,2048,1024)')
pg.wait_for_function('GAME.running',timeout=60000)
pg.evaluate('''() => { weather.next=1e15; SET.zoom=3; resizeRender(); bot.x=880;bot.y=275; snapCamera();
document.querySelectorAll('.card,.toast').forEach(e=>e.remove());$('hud').hidden=true;
globalThis.lakeSave=serializeWorld(); }''')
assert pg.evaluate('treeBases.every(i=>!treeHeld(i)||mat[i]!==WATER)')
assert pg.evaluate('mat.every((m,i)=>m!==WATER||awake[((i/W)>>5)*CW+((i%W)>>5)]>0)')
measure = '''() => { const pools=[]; let p=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){p=null;continue;}
if(!p){p={width:0,min:y,max:y};pools.push(p);}
p.width++;p.min=Math.min(p.min,y);p.max=Math.max(p.max,y);}
return pools.filter(p=>p.width>20); }'''
pools=pg.evaluate(measure)
print('fresh startup',pools,flush=True)
assert pools and all(p['max']-p['min']<=1 for p in pools)
pg.wait_for_timeout(300)
(ROOT/'shots').mkdir(exist_ok=True)
pg.screenshot(path=str(ROOT/'shots/t96-level.png'))
for state in ('fresh','reloaded'):
if state=='reloaded':
pg.evaluate('''() => {deserializeWorld(lakeSave.meta,lakeSave.buffers);weather.next=1e15;}''')
pg.evaluate('startThreads()')
for batch in range(3):
pg.evaluate('for(let k=0;k<200;k++)simTick();')
result=pg.evaluate('''() => {let lo=H,hi=0,n=0,wet=0;
for(let x=650;x<1100;x++){let y=0;while(y<H&&mat[y*W+x]===EMPTY)y++;
if(mat[y*W+x]===WATER){lo=Math.min(lo,y);hi=Math.max(hi,y);n++;}}
for(const i of treeBases)if(i%W>=650&&i%W<1100&&treeHeld(i)&&mat[i]===WATER)wet++;
return {lo,hi,n,wet,workers:simPool?.workers.length};}''')
pools=pg.evaluate(measure)
assert pools and all(p['max']-p['min']<=1 for p in pools),pools
print(state,result,pools,flush=True)
assert result['n']>300 and result['hi']-result['lo']<=1 and result['wet']==0
pg.evaluate('snapCamera();renderDirty.fill(1);')
pg.wait_for_timeout(300)
pg.screenshot(path=str(ROOT/f'shots/t96-{state}-settled.png'))
assert not errors,errors
print('PASS t96 pixel flat startup and 600-tick fresh/reloaded lakes, dry roots, 2 browser helpers, no page errors',flush=True)