From fc292141623ee8730e94e1317f0a913823bd20e0 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Thu, 29 Oct 2026 18:21:45 -0400 Subject: [PATCH] New-world lakes start level; no trees in water (t96) --- docs/DESIGN.md | 7 +++++++ docs/PROJECT_MAP.md | 3 ++- docs/TESTING.md | 5 +++++ docs/agent-log.md | 12 +++++++++++ src/sim/worldgen.js | 21 +++++++++++++++++-- test/t_lakes.js | 51 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 test/t_lakes.js diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 706f3a1..54e971a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -590,6 +590,13 @@ above, including: ordinary repose rules take over. Keep stamped moves, barriers, burning-bed protection and worker independence. The existing worldgen POWDER_FAN switch disables new face releases. +- **New-world lakes wake immediately; keep the seed layout (t96, 2026-10-29).** Keep the + recorded worldgen settling speeds and material/temperature output. Wake water after the + final startup reset so unfinished lakes level without drilling. Retain tree candidates and + their random draws, then omit whole trees whose bases are wet or on an open bank connected + to water at base height after settling. This keeps trees out of the lake as it spreads. + Existing load wake scheduling and later snowmelt/flooding rules stay as they are. + ## Ideas not built yet - **Concrete**, as a stronger wall material from rubble. diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index c4a64f6..5d20dbf 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -383,7 +383,7 @@ World generation helpers: noise and removal of tiny hard rock specks. - functions: makeNoise, fbm, clearHardSpecks - variables: SPECK_MIN -### src/sim/worldgen.js (286 lines) +### src/sim/worldgen.js (303 lines) World generation: staged terrain, ore, caves and placement, with synchronous and async entry points. - functions: generateWorld, generateWorldAsync @@ -656,6 +656,7 @@ The frame loop and boot. - `test/t_gpu_golden.py`: Frozen world references; seven smooth crops corrected for continuous noise in t91. - `test/t_gpu_light.py`: The former CPU light behavioural checks, now read from the actual GPU light grid. - `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables. +- `test/t_lakes.js`: Generated lakes wake without a tool, settle on their own, and have dry tree bases. - `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state. - `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe. - `test/t_nozzle.js`: Blowing aims within the nozzle's reach, with room for the brush and settling. diff --git a/docs/TESTING.md b/docs/TESTING.md index 35fe880..d853703 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -69,6 +69,11 @@ seeded sand pile bounds in `t_corner_seal.js`; the heat oracle now supports pass The conveyor fixture uses a wider feed pile, the screen fixture warms longer, and `t_load.js` checks waking within eight ticks separately from the probabilistic consumption of fuel. +`test/t_lakes.js` pins SHA-256 over raw `mat` followed by Float32 `temp` bytes for seeds +7, 42 and 424242 at 2048x1024. These hashes predate t96 and must not be re-baselined for a +wake/background-tree change. It also checks generated tree bases, startup wakes, and the +seed-7 lake leveling within two grains after 6,000 undisturbed ticks, fresh and reloaded. + ## Browser tests - They need the `.venv` from README.md → Develop; plain `python3` switches to it on its own. diff --git a/docs/agent-log.md b/docs/agent-log.md index 7c9ffb9..eddf899 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 wake new-world lakes and keep trees off their banks (Codex) + +- Wake water after startup reset; omit whole tree candidates on wet/water-connected low banks. + Original mat+temp hashes match for seeds 7/42/424242; pinned settling, physics and save format unchanged. +- Files: src/sim/worldgen.js, test/t_lakes.js, docs/{DESIGN,TESTING,PROJECT_MAP}.md, tasks/t96* and this log. +- Tests failed first; all 38 headless suites pass (23 lake checks; /tmp/t96-headless.log). + Only browser test: /tmp/playwright-test-t96.py (fresh/reloaded seed 7, pixel, two helpers, no page errors; + /tmp/t96-browser-final.log). Inspected shots/t96-{fresh,fresh-settled,reloaded-settled}.png. + Sandbox graphics startup stalled; outside-sandbox run passed. No full browser suite. +- Lake moves on tick 1; levels within two grains by 6,000 ticks (100 seconds). Later snowmelt + can flood other trees; that behavior is unchanged. Owner playtest pending. No commit. + ## 2026-10-29: t95 separate nozzle and rain flight allowances (Codex) - Nozzle counts bag flyers only, up to 4,000, with “Too much in the air.” feedback; diff --git a/src/sim/worldgen.js b/src/sim/worldgen.js index d7eb1bc..7b38961 100644 --- a/src/sim/worldgen.js +++ b/src/sim/worldgen.js @@ -190,12 +190,14 @@ function* worldSteps(seedIn, w, h) { } yield [0.72, 'Snow and trees']; // trees + const trees = []; for (let x = 12; x < w - 12;) { const sy = surfY[x]; const ok = mat[sy * w + x] === GRASS && mat[sy * w + x + 1] === GRASS && !noTree[x] && Math.abs(x - spawnX) > 6 && !(x > spawnX + 14 && x < spawnX + 72); // none where the dock goes if (ok && rng() < 0.7) { + const tree = { x, y: sy - 1, cells: [] }; trees.push(tree); const th = 9 + Math.floor(rng() * 8); - const setW = (xx, yy) => { if (xx < 0 || yy < 1 || xx >= w) return; const i = yy * w + xx; if (mat[i] === EMPTY) bgm[i] = WOOD; }; + const setW = (xx, yy) => { if (xx < 0 || yy < 1 || xx >= w) return; const i = yy * w + xx; if (mat[i] === EMPTY) { bgm[i] = WOOD; tree.cells.push(i, WOOD); } }; for (let k = 1; k <= th; k++) for (let dx = 0; dx < 2; dx++) setW(x + dx, sy - k); // two branches for (const side of [-1, 1]) { const by = sy - th + 3 + Math.floor(rng() * 3); for (let k = 1; k <= 3; k++) setW(x + (side < 0 ? -k : 1 + k), by - (k >> 1)); } @@ -206,7 +208,7 @@ function* worldSteps(seedIn, w, h) { for (let yy = Math.floor(cy - r); yy <= cy + r; yy++) for (let xx = Math.floor(cx - r); xx <= cx + r; xx++) { if (xx < 0 || yy < 1 || xx >= w) continue; const ex = (xx - cx) / r, ey = (yy - cy) / (r * 0.85); - if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) { const i = yy * w + xx; if (mat[i] === EMPTY && !bgm[i]) bgm[i] = LEAVES; } + if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) { const i = yy * w + xx; if (mat[i] === EMPTY && !bgm[i]) { bgm[i] = LEAVES; tree.cells.push(i, LEAVES); } } } } x += 16 + Math.floor(rng() * 20); @@ -258,9 +260,24 @@ function* worldSteps(seedIn, w, h) { // re-ambient after settling, then start clean for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; } tick = 0; + // Settling can carry lakes past noTree; leave their connected low banks treeless too. + bgm.fill(0); + for (const tree of trees) { + let wet = false; + for (let root = tree.x; root <= tree.x + 1 && !wet; root++) for (const d of [-1, 1]) { + for (let x = root; x >= 0 && x < w; x += d) { + const m = mat[tree.y * w + x]; + if (m === WATER) { wet = true; break; } + if (m !== EMPTY && PH[m] !== PH_GAS) break; + } + } + if (!wet) for (let k = 0; k < tree.cells.length; k += 2) bgm[tree.cells[k]] = tree.cells[k + 1]; + } findTreeBases(); placeDock(spawnX + 34); hot.fill(0); awake.fill(0); updateRoof(); + // Keep the generated grains intact, but let unfinished lakes level from the first tick. + for (let i = 0; i < N; i++) if (mat[i] === WATER) wakeIdx(i); resetEconomy(); resetWeather(); weather.base = worldWater(); bot.x = spawnX - 2; bot.y = surfY[spawnX] - 14; bot.vx = 0; bot.vy = 0; diff --git a/test/t_lakes.js b/test/t_lakes.js new file mode 100644 index 0000000..95b5d31 --- /dev/null +++ b/test/t_lakes.js @@ -0,0 +1,51 @@ +// Generated lakes wake without a tool, settle on their own, and have dry tree bases. +const { load, suite, ticks } = 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; + 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); } + pool.x1 = x; pool.width++; pool.min = Math.min(pool.min, y); pool.max = Math.max(pool.max, y); + } + return pools.filter(p => p.width > 20); + })()`); +} +for (const [seed, expected] of [ + [7, '865ac320f743ff5ed573acab3333773b5ebc2e10f15600438dd15f1767acb72e'], + [42, '94930e108f7bd948c6f3dbe8b15b2950eed695cb3d57376d05a8ad781c2015b7'], + [424242, '8b8a4b44c2cc98d3ab86515eb55f38ce7cb3d6a283e29e0ed758af937ea8e8c8'], +]) { + 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)); + check(`seed ${seed} preserves generated material and temperature bytes`, hash.digest('hex') === expected); + 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)`)); + } + } +} +done();