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:
+9
-6
@@ -590,12 +590,15 @@ 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.
|
||||
- **Level lakes during world generation (t96, 2026-10-29).** The owner wants new worlds
|
||||
to start with flat lakes instead of waiting 80–125 seconds for them to flow level. Changed
|
||||
generated worlds are accepted: seeds no longer reproduce the old water layout. After the
|
||||
existing settling loop, level four-connected water bodies of at least 50 grains, preserving
|
||||
their exact volume. Fill reachable air/water from the bottom upward, respecting solid/powder
|
||||
walls and dry rims; place a partial top row from the middle outward. Establish the dock's
|
||||
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.
|
||||
|
||||
## Ideas not built yet
|
||||
|
||||
|
||||
+10
-8
@@ -322,9 +322,9 @@ Bars and other loose objects (ents), and the bag: adding, taking, costs and refu
|
||||
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entBeltScrewEnter, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
||||
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
||||
|
||||
### src/sim/tools.js (65 lines)
|
||||
### src/sim/tools.js (30 lines)
|
||||
Tool helpers: reach, brushes, and plain digging.
|
||||
- functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig
|
||||
- functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach
|
||||
- variables: BUILD_MAX, buildBox
|
||||
|
||||
### src/sim/drill.js (183 lines)
|
||||
@@ -378,12 +378,12 @@ The same chunk schedule runs inline or in workers. Workers only own a pass at a
|
||||
simTick: one simulation step, in order.
|
||||
- functions: simTick
|
||||
|
||||
### src/sim/worldgen-helpers.js (61 lines)
|
||||
### src/sim/worldgen-helpers.js (131 lines)
|
||||
World generation helpers: noise and removal of tiny hard rock specks.
|
||||
- functions: makeNoise, fbm, clearHardSpecks
|
||||
- functions: makeNoise, fbm, clearHardSpecks, levelWorldWater
|
||||
- variables: SPECK_MIN
|
||||
|
||||
### src/sim/worldgen.js (303 lines)
|
||||
### src/sim/worldgen.js (305 lines)
|
||||
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
||||
- functions: generateWorld, generateWorldAsync
|
||||
|
||||
@@ -397,9 +397,9 @@ Saving and loading a world (serializeWorld / deserializeWorld) and old-save migr
|
||||
- functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick
|
||||
- variables: SAVE_VERSION, TOFF
|
||||
|
||||
### src/render/setup.js (190 lines)
|
||||
### src/render/setup.js (188 lines)
|
||||
Drawing (src/render/): browser only. This file: the renderer state R, colour tables (glow, thermal palettes, pipes, trees, sky), canvas setup and the camera.
|
||||
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont
|
||||
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont
|
||||
- variables: R, GLOW_W, GLOW_R, GLOW_G, GLOW_B, THERM, PC, TREE_R, TREE_G, TREE_B, BG, SKY_R, SKY_G, SKY_B
|
||||
|
||||
### src/render/slope-belts.js (167 lines)
|
||||
@@ -664,7 +664,9 @@ The frame loop and boot.
|
||||
- `test/t_gpu_light.py`: The former CPU light behavioural checks, now read from the actual GPU light grid.
|
||||
- `test/t_import_save.js`: Import must preserve the running world before reading or replacing it.
|
||||
- `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_lakes.py`: Pixel startup and fresh/reloaded flat lakes after 600 ticks; shots/t96-level.png.
|
||||
- `test/t_lakes.js`: Generated lakes start flat, conserve water, and have dry tree bases.
|
||||
- `test/t_lakes.py`: t96: flat generated lakes, fresh and reloaded, in the real pixel frame loop.
|
||||
- `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.
|
||||
|
||||
+8
-3
@@ -76,9 +76,14 @@ The conveyor fixture uses a wider feed pile, the screen fixture warms longer, an
|
||||
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.
|
||||
7, 42 and 424242 at 2048x1024. t96 round 2 re-baselines only for the lake-levelling pass;
|
||||
`t_corner_seal.js` also pins three smaller worlds with the same intentional change. The lake
|
||||
suite checks all five seeds (also 2 and 1): surface pools wider than 20 grains start within
|
||||
one grain of level and stay so after 600 ticks, exact WATER count from the pre-pass 0.99
|
||||
progress callback, dry tree bases and startup wakes. Small fixtures cover solid/powder rims,
|
||||
volume, centered partial rows, the 50-grain cutoff and diagonal separation.
|
||||
`python3 test/t_lakes.py` checks pixel startup and fresh/reloaded lakes after 600 ticks with
|
||||
two browser helpers and no page errors; `shots/t96-level.png` shows the untouched new lake.
|
||||
|
||||
`test/t_bot_rescue.js` covers deep basalt burial, nearest/upward and diagonal nudges,
|
||||
unexplored-pocket rejection, newest open trail points, ring wrap/load reset, bounded searches,
|
||||
|
||||
@@ -4,6 +4,26 @@ 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 2 — level lakes during generation (Codex)
|
||||
|
||||
- Level 50+ grain water bodies with typed-array flood fills, exact volume and centered partial
|
||||
rows; keep settling switches, startup wakes and tree rules. Establish the existing dock
|
||||
foundation first (otherwise seed 42 loses 39 grains). Recorded the owner's changed-seed decision.
|
||||
- Files: src/sim/worldgen{,-helpers}.js, test/t_lakes.{js,py}, test/t_corner_seal.js,
|
||||
docs/{DESIGN,TESTING,PROJECT_MAP}.md, tasks/t96* and this log.
|
||||
- Eight regressions failed first; all 40 headless suites pass (59 lake checks). Six world hashes
|
||||
re-baselined only for WATER/EMPTY redistribution; bypassing the pass restores all six originals.
|
||||
Pixel browser fresh/reload seed 7 at 600 ticks passes with two helpers and no page errors.
|
||||
Inspected shots/t96-level.png: flat lake, centered one-grain top row, dry bank trees, dock at right.
|
||||
- Paired generation medians before→after (ms), seeds 7/42/424242: 871.09→903.88,
|
||||
921.47→981.33, 1067.50→1119.16. Pass medians 13.67/22.17/15.27 ms; max 24.05
|
||||
measured / 41.22 cold. Evidence: /tmp/t96-round2-{red,headless-final,browser,paired-times}.log.
|
||||
Review pending; no full browser suite, no commit.
|
||||
- Claude finished the run after Codex hit its usage limit: registered test/t_lakes.py in run_all.py's
|
||||
browser list; full `run_all.py --browser` passed except the known flaky t_smooth.py (t99), which
|
||||
passed 3/3 reruns. Found that 4 of 12 seeds' levelled lakes now reach the trade dock's wall
|
||||
(none enter it); cause is the lake lip using the highest rim point (see t96).
|
||||
|
||||
## 2026-10-29: t69 hot-load warning, visible damage and build-over repair (Codex)
|
||||
|
||||
- Added persistent scorched/fractured screw overlays in both looks, 900°C load warnings,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// World generation helpers: noise and removal of tiny hard rock specks.
|
||||
// World generation helpers: noise, hard rock specks and lake levelling.
|
||||
// ---------- Perlin noise ----------
|
||||
function makeNoise(rng) {
|
||||
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
|
||||
@@ -59,3 +59,73 @@ function clearHardSpecks(live) {
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Grow each basin upward from its bottom: dry walls/rims are crossed only if filled to them.
|
||||
function levelWorldWater() {
|
||||
const seen = new Uint8Array(N), region = new Uint32Array(N), cells = new Int32Array(N), stack = new Int32Array(N);
|
||||
let tag = 0;
|
||||
for (let start = 0; start < N; start++) {
|
||||
if (mat[start] !== WATER || seen[start]) continue;
|
||||
let size = 0, sp = 1, bottom = start;
|
||||
stack[0] = start; seen[start] = 1;
|
||||
// Measure the original four-connected body before moving any of its water.
|
||||
while (sp) {
|
||||
const i = stack[--sp], x = i % W, y = (i / W) | 0;
|
||||
cells[size++] = i;
|
||||
if (i > bottom) bottom = i;
|
||||
for (let d = 0; d < 4; d++) {
|
||||
if ((d === 0 && !x) || (d === 1 && x === W - 1) || (d === 2 && !y) || (d === 3 && y === H - 1)) continue;
|
||||
const j = i + (d === 0 ? -1 : d === 1 ? 1 : d === 2 ? -W : W);
|
||||
if (mat[j] === WATER && !seen[j]) { seen[j] = 1; stack[sp++] = j; }
|
||||
}
|
||||
}
|
||||
if (size < 50) continue;
|
||||
let volume = size;
|
||||
for (let k = 0; k < size; k++) { const i = cells[k]; mat[i] = EMPTY; flags[i] = 0; }
|
||||
tag++; size = sp = 1; cells[0] = stack[0] = bottom; region[bottom] = tag;
|
||||
for (let level = (bottom / W) | 0; level >= 0; level--) {
|
||||
if (!sp && level < H - 1) {
|
||||
for (let x = 0; x < W; x++) {
|
||||
const i = level * W + x;
|
||||
if (region[i + W] === tag && (mat[i] === EMPTY || mat[i] === WATER)) {
|
||||
region[i] = tag; cells[size++] = i; stack[sp++] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
// At this height, fill every connected low space before rising another row.
|
||||
while (sp) {
|
||||
const i = stack[--sp], x = i % W, y = (i / W) | 0;
|
||||
if (mat[i] === WATER) { volume++; seen[i] = 1; }
|
||||
for (let d = 0; d < 4; d++) {
|
||||
if ((d === 0 && !x) || (d === 1 && x === W - 1) || (d === 2 && y <= level) || (d === 3 && y === H - 1)) continue;
|
||||
const j = i + (d === 0 ? -1 : d === 1 ? 1 : d === 2 ? -W : W);
|
||||
if (region[j] === tag || (mat[j] !== EMPTY && mat[j] !== WATER)) continue;
|
||||
region[j] = tag; cells[size++] = j; stack[sp++] = j;
|
||||
}
|
||||
}
|
||||
if (size >= volume) break;
|
||||
}
|
||||
const body = cells.subarray(0, size);
|
||||
body.sort(); // row order, with no objects allocated per grain
|
||||
for (let k = 0; k < size; k++) { const i = cells[k]; mat[i] = EMPTY; flags[i] = 0; seen[i] = 1; }
|
||||
for (let end = size; end > 0 && volume > 0;) {
|
||||
const y = (cells[end - 1] / W) | 0;
|
||||
let begin = end - 1;
|
||||
while (begin > 0 && ((cells[begin - 1] / W) | 0) === y) begin--;
|
||||
const width = end - begin;
|
||||
if (volume >= width) {
|
||||
for (let k = begin; k < end; k++) { mat[cells[k]] = WATER; flags[cells[k]] = F_NAT; }
|
||||
volume -= width;
|
||||
} else {
|
||||
const left = cells[begin] % W, right = cells[end - 1] % W, mid = (left + right) >> 1;
|
||||
for (let d = 0; volume > 0; d++) {
|
||||
const x = mid + (d & 1 ? (d + 1) >> 1 : -(d >> 1));
|
||||
if (x < left || x > right) continue;
|
||||
const i = y * W + x;
|
||||
if (region[i] === tag) { mat[i] = WATER; flags[i] = F_NAT; volume--; }
|
||||
}
|
||||
}
|
||||
end = begin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -256,7 +256,10 @@ function* worldSteps(seedIn, w, h) {
|
||||
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
||||
}
|
||||
LSTEP = lstep; LFILM = lfilm; SORTP = sortp; CORNER_SEAL = cornerSeal; POWDER_FAN = powderFan;
|
||||
// Establish the dock's ground before levelling, so its foundation cannot erase water.
|
||||
placeDock(spawnX + 34);
|
||||
yield [0.99, 'Finishing'];
|
||||
levelWorldWater();
|
||||
// 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;
|
||||
@@ -274,9 +277,8 @@ function* worldSteps(seedIn, w, h) {
|
||||
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.
|
||||
// Wake generated water so it responds from the first tick.
|
||||
for (let i = 0; i < N; i++) if (mat[i] === WATER) wakeIdx(i);
|
||||
resetEconomy(); resetWeather();
|
||||
weather.base = worldWater();
|
||||
|
||||
+1
-1
@@ -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,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
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user