New-world lakes start level; no trees in water (t96)

This commit is contained in:
2026-10-29 18:21:45 -04:00
parent d936f65553
commit fc29214162
6 changed files with 96 additions and 3 deletions
+7
View File
@@ -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.
+2 -1
View File
@@ -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.
+5
View File
@@ -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.
+12
View File
@@ -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;
+19 -2
View File
@@ -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;
+51
View File
@@ -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();