diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 0b0769e..d3322c2 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -599,6 +599,14 @@ above, including: existing foundation first so it cannot erase newly levelled water. Keep the pinned settling values, startup water wakes, and round-1 wet/water-connected-bank tree filtering. Existing load wakes and later snowmelt/flooding rules stay as they are. +- **Lakes fill only to their lower rim and keep clear of spawn (t96 round 3, 2026-10-29).** + The generator took the basin's `lip` as `Math.min(surfY)`, the highest ground (y grows + downward), so water was poured above the lower rim and ran out toward low ground; once + levelled it reached the trade dock on 4 of 12 seeds. The owner chose option 2: the lip is + now the lower of the two rim edges, minus one, so a lake is filled to just under that rim, + and each lake's half-width is capped so its edge stays 80 grains from spawn (and the dock). + Lakes are smaller and lower: about 180-250 grains wide instead of 250-330, 5-15 grains + lower. The random draws keep their order. ## Ideas not built yet diff --git a/docs/TESTING.md b/docs/TESTING.md index 3964118..9ba76a5 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -124,7 +124,8 @@ screenshots go to `shots/t72-{retrace,escape,open}.png`. joins 2, sky 12/2, lamp 12, magma 12): the old noise had seams in the references too. The remaining crops are frozen pre-removal legacy images. The compressed `golden-world-640x*.json.gz` fixtures retain the Phase 2 world bytes that produced them, - isolating renderer checks from worldgen RNG changes. Inspect the matching + isolating renderer checks from worldgen RNG changes; t96 round 3 added the old `surfY`, `dirtY` and `AMB` + columns too, because the renderer reads them and lake generation changed them. Inspect the matching `shots/golden-*.png` after visible changes. - `test/t_gpu.py` checks default limits (four storage textures per stage), dirty-upload readback after 240 busy ticks with scrolling and both looks, exact dirty/full image equality, diff --git a/docs/agent-log.md b/docs/agent-log.md index 4e9b9e5..00c729c 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,18 @@ What agents have done, newest first. Add an entry for every piece of work: the d note or request it was for, what changed, the files touched, the tests run, and anything left open or not verified. Keep each entry short; the git history has the details. +## 2026-10-29: t96 round 3, lakes fill to their lower rim (Claude) + +- Owner chose option 2. src/sim/worldgen.js: lake `lip` is the lower rim edge minus one (was + the highest ground in the basin), and lake half-width is capped to keep 80 grains from spawn + and the dock. Same random draws. Lakes are smaller and stay off the dock (seeds 5/6/7/42 used to + reach it). +- Tests: t_lakes.js adds seeds 5 and 6 and a dock-clearance check (fails on the old generator for + 7/42/5/6); hashes re-baselined in t_lakes.js, t_corner_seal.js and t_world.js (shallow ore: + 414 of 28,291 cells changed, all under the two new lake basins). t_lakes.py width threshold + 300 -> 150. Golden fixtures now also freeze surfY/dirtY/AMB from the previous generator. +- Full `run_all.py --browser`: see the commit; t_smooth.py is the known flake (t99). + ## 2026-10-29: t96 round 2 — level lakes during generation (Codex) - Level 50+ grain water bodies with typed-array flood fills, exact volume and centered partial diff --git a/src/sim/worldgen.js b/src/sim/worldgen.js index 4fd89b3..4acc0f6 100644 --- a/src/sim/worldgen.js +++ b/src/sim/worldgen.js @@ -67,12 +67,13 @@ function* worldSteps(seedIn, w, h) { else dirtY[x] = Math.min(dirtY[x], surfY[x] + 20); } } - // lakes: a basin dug below the lowest point of its rim, filled to just above it + // lakes: a basin dug below the lower of its two rims, filled to just under that rim (y grows + // downward, so the lower rim is the larger surfY); kept 80 grains clear of spawn and the dock const lakeTop = new Int16Array(w).fill(h), noTree = new Uint8Array(w), desAt = new Uint8Array(w); for (const f of feats) { if (f.k !== 'lake') continue; - const lw = Math.round(90 + rng() * 40), deep = 22 + rng() * 8; - let lip = 1e9; for (let x = f.x - lw; x <= f.x + lw; x++) if (x >= 0 && x < w) lip = Math.min(lip, surfY[x]); + const lw = Math.min(Math.round(90 + rng() * 40), Math.abs(f.x - spawnX) - 80), deep = 22 + rng() * 8; + const lip = Math.max(surfY[Math.max(0, f.x - lw)], surfY[Math.min(w - 1, f.x + lw)]) - 1; for (let x = f.x - lw; x <= f.x + lw; x++) { if (x < 0 || x >= w) continue; const u = (x - f.x) / lw, depth = Math.max(0, 1 - u * u) * deep; diff --git a/test/fixtures/golden-world-640x384.json.gz b/test/fixtures/golden-world-640x384.json.gz index 5023b19..cff03d7 100644 Binary files a/test/fixtures/golden-world-640x384.json.gz and b/test/fixtures/golden-world-640x384.json.gz differ diff --git a/test/fixtures/golden-world-640x512.json.gz b/test/fixtures/golden-world-640x512.json.gz index f189302..dacc898 100644 Binary files a/test/fixtures/golden-world-640x512.json.gz and b/test/fixtures/golden-world-640x512.json.gz differ diff --git a/test/t_corner_seal.js b/test/t_corner_seal.js index 86db030..2c7f552 100644 --- a/test/t_corner_seal.js +++ b/test/t_corner_seal.js @@ -4,11 +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. +// t96 re-baseline: only lake generation (rim height, spawn clearance, levelling) changes these material bytes. for (const [seed, expected] of [ - [777, '1fac6b4b6420f328d7de87c0813e328303271e7605d85ce541e108361aff0d90'], - [77, 'de3ec38547585486924f53abb82cb3c0f76e0bd24b75873ddf96599b5886b52a'], - [4242, '57cd89b5518c3e342fc1b91243cd068dc2e38a2d42de84a1c2ee1cbf936c2f1a'], + [777, '7d60a0f1a92b42438471c8c15c5cc92576a75a4aa54b8c519c42904acb7088a6'], + [77, '724eb1730ca5fead6aeaa13994beb2e066bb9d65c7db7e0b790857e2d72b7c2a'], + [4242, '9ffdabf1ffe8ab6cd5855ccfe05c31221d1a0517d960bf2e2273018e9dbf03f9'], ]) { run(`generateWorld(${seed}, 512, 384)`); check(`seed ${seed} restores corner sealing`,run('CORNER_SEAL')===true); diff --git a/test/t_lakes.js b/test/t_lakes.js index a9e13c7..fd02f47 100644 --- a/test/t_lakes.js +++ b/test/t_lakes.js @@ -17,11 +17,14 @@ function surfacePools() { })()`); } for (const [seed, expected] of [ - [7, '669a4ee87496be27473d2987d9a8935be1a19b29a8f2433d306f32a1616f3bca'], - [42, 'efeadd2bff0ce3eaf81e5a08a998ce767e9667e3b27b8623e928f202669f640e'], - [424242, '24b5e6ccc4e7d5eb9d54c3ff0fa5dfe84185b83e75b07261ac007b4f920f0640'], + [7, '4c4778417e6cf6e9577b831e66111bd374068a14e15642bae304b8a45a68b879'], + [42, '907f2ed79b9fda1ae99c5ee55fa19ce85eff8ffdcd7dadac14a2deb42223217d'], + [424242, '40080ea6039d0d4247e36fd9a5837d0675100414f198d77419ecf500c7f9d6ad'], [2, null], [1, null], + // Before t96 round 3 these lakes 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); @@ -33,6 +36,8 @@ for (const [seed, expected] of [ 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} 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)); 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)')); diff --git a/test/t_lakes.py b/test/t_lakes.py index d982a9c..7c96575 100644 --- a/test/t_lakes.py +++ b/test/t_lakes.py @@ -47,7 +47,7 @@ with launch(viewport={'width':1280,'height':760}) as (_, pg, url): 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 + assert result['n']>150 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')) diff --git a/test/t_world.js b/test/t_world.js index 631db5d..a0f82c8 100644 --- a/test/t_world.js +++ b/test/t_world.js @@ -185,7 +185,8 @@ oreNames.forEach((name, j) => { }); const totalOre = oreBalance.counts.reduce((a, b) => a + b, 0); check('total ore is about 4 to 4.5 times the original', totalOre >= 162519 * 4 && totalOre <= 162519 * 4.5, totalOre); -check('shallow ore matches the original generator', oreBalance.shallowHash === 706875613, oreBalance.shallowHash); +// t96 round 3: lake basins moved, so ore under them changed (414 cells, all under the two lakes). +check('shallow ore matches the original generator', oreBalance.shallowHash === 607138519, oreBalance.shallowHash); run('generateWorld(99, 4096, 1536, null)'); const marks = run('(()=>{let lakes=0,peaks=0,inLake=false;for(let x=1;x= 2 && marks[1] >= 2, marks);