Water pours off ledges several times faster, raining clouds spread boiled-off water (t21)

- Liquids step up to 8 cells a tick toward a drop (fewer the thicker they are), and a top-layer
  grain looks 4 times as far for one, so pools empty and thin film drains. A 100-wide pool
  empties in about 50 s; the old code left 24-650 grains stranded on wide ledges.
- Only a top-layer grain stepping sideways wakes the row ahead, so settled lakes still sleep.
- World generation settles at the old speed: seeds make the same worlds as before.
- Cooled steam under a roof no longer stays steam for ever (its heat area slept).
- Raining clouds keep drifting at half pace and rain over a band either side, so a waterfall into
  a hot shaft no longer rains straight back on its own lake.
- New test/t_water.js; t_weather rain-location checks; t_conveyors counts steam in flight;
  t_molds seeds the random burning-bed check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 20:44:06 -04:00
co-authored by Claude Opus 5.5
parent eee170e3d1
commit 9291040ff0
10 changed files with 195 additions and 20 deletions
+13
View File
@@ -338,6 +338,19 @@ above, including:
version's narrow fade too sharp. version's narrow fade too sharp.
Unexplored ground is pure black, not dark grey (the owner: "should be black anyway"). Unexplored ground is pure black, not dark grey (the owner: "should be black anyway").
- **Water pours faster, and boiled-off rain spreads out (t21, 2026-10-24).** The owner found water
running off a ledge slower than expected. Only the top layer of a pool flows, so pools emptied
at a trickle and the old code left 24-650 grains stranded on a wide ledge for good.
- A liquid now steps up to 8 cells a tick toward a drop instead of 1, fewer the thicker it is
(water 8, molten copper 5, slag 4, magma 2, glass 1). A grain on top may look 4 times as far
for a drop, so thin film left behind drains too. A 100-wide pool empties in about 50 s
(it never fully emptied before); settled lakes still sleep.
- Raining clouds keep drifting downwind at half pace, and rain falls over a cloud's band and the
band either side. Before, a waterfall into a shaft hot enough to boil it could rain straight
back on its own lake.
- World generation settles the land at the old water speed, so every seed keeps making the
same world.
## Ideas not built yet ## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble. - **Concrete**, as a stronger wall material from rubble.
+5 -4
View File
@@ -226,10 +226,10 @@ Exploration only: never changes grains or the random stream.
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll - functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH - variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
### src/sim/cells.js (176 lines) ### src/sim/cells.js (187 lines)
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove). Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove - functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove
- variables: SORTP - variables: SORTP, LSTEP, LFILM
### src/sim/heat.js (246 lines) ### src/sim/heat.js (246 lines)
Heat: conduction, melting and freezing, reactions, fire and air, steel. Heat: conduction, melting and freezing, reactions, fire and air, steel.
@@ -320,7 +320,7 @@ The bot: movement and collision with the world.
Milestone checks (the ledger) and the sky roof line used for cooling. Milestone checks (the ledger) and the sky roof line used for cooling.
- functions: checkLedger, updateRoof - functions: checkLedger, updateRoof
### src/sim/weather.js (85 lines) ### src/sim/weather.js (87 lines)
Weather: clouds from steam, rain and snow, and the world water count. Weather: clouds from steam, rain and snow, and the world water count.
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick - functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
- variables: WBAND, WS, weather - variables: WBAND, WS, weather
@@ -329,7 +329,7 @@ Weather: clouds from steam, rain and snow, and the world water count.
simTick: one simulation step, in order. simTick: one simulation step, in order.
- functions: simTick - functions: simTick
### src/sim/worldgen.js (344 lines) ### src/sim/worldgen.js (346 lines)
World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks. World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks.
- functions: makeNoise, fbm, clearHardSpecks, generateWorld, generateWorldAsync - functions: makeNoise, fbm, clearHardSpecks, generateWorld, generateWorldAsync
- variables: SPECK_MIN - variables: SPECK_MIN
@@ -476,6 +476,7 @@ The frame loop and boot.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation. - `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works. - `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders. - `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing. - `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
- `test/t_world.js`: Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag refunds, drill reach, ballistic blowing, snowy mountains. - `test/t_world.js`: Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag refunds, drill reach, ballistic blowing, snowy mountains.
- `tools/map.py`: Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md. - `tools/map.py`: Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md.
+20
View File
@@ -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 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. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-24: t21 phase 1: water test pass, faster pouring, rain spreads (Claude Code, Codex)
- `test/t_water.js` (20 checks): conservation, levelling and sleep, pools pouring off ledges,
world edges and corners, pumps, freezing and thawing, boiling and condensing, a pool draining
into a hot shaft.
- `liquid()`/`move()` in `src/sim/cells.js`: multi-cell steps (`LSTEP`, scaled by `FLUID`),
a longer reach for top-layer grains (`LFILM`), and waking the row ahead only when a top-layer
grain steps sideways (waking on every liquid move kept 207 of 2048 areas busy after a reload).
`worldSteps` settles with both at 1: seeds 777, 77 and 4242 hash the same as before.
- Found: cooled steam drifting under a roof could stay steam for ever, because its heat area
slept. `gas()` now keeps the heat pass on it.
- `weatherTick()`: raining clouds drift at half pace; rain falls over the band and one either
side. `t_weather`: 'close to where it gathered' now samples drops in the air (landed water
spreads); a new wind check.
- Test fixes, not weakening: `t_conveyors` pit check counts steam on its way up (the water total
was exact); `t_molds` burning-bed check seeds `RS` (its result hung on earlier tests' draws:
0.28-1.04 depending on the sequence, identical on the old and new code).
- Steady cost after a reload (2048x1024): 1.26 -> 1.24 ms/tick. Tests: `run_all.py --browser`
ALL PASSED. Screenshots `shots/t21-pour-120.png`, `shots/t21-pour-900.png`.
## 2026-10-24: t1: bars stack neatly, Rack blocks store them (Claude Code, Codex) ## 2026-10-24: t1: bars stack neatly, Rack blocks store them (Claude Code, Codex)
- `232f5af`: `entTick` snap-and-tip (`entSupportCount`, `src/sim/items.js`); - `232f5af`: `entTick` snap-and-tip (`entSupportCount`, `src/sim/items.js`);
+15 -4
View File
@@ -21,6 +21,13 @@ function swapCells(i, j) {
function move(i, j, x, y, nx, ny) { function move(i, j, x, y, nx, ny) {
swapCells(i, j); swapCells(i, j);
wakeXY(x, y); wakeXY(nx, ny); wakeXY(x, y); wakeXY(nx, ny);
if (ny === y && LFILM > 1 && PH[mat[j]] === PH_LIQUID && (y === 0 || PH[mat[i - W]] !== PH_LIQUID)) {
// a top-layer grain stepping sideways may clear the path to a drop for surface grains far
// along the row, so wake them; moves inside a body of water do not (settled lakes must sleep)
const reach = SPREAD[mat[j]] * LFILM, row = (y >> 5) * CW;
const first = Math.max(0, x - reach) >> 5, last = Math.min(W - 1, x + reach) >> 5;
for (let cx = first; cx <= last; cx++) awake[row + cx] = 3;
}
if (part[j] === P_WHEEL) { const mm = machines[mach[j]]; if (mm) mm.flow += PH[mat[j]] === PH_LIQUID ? 1 : 0.5; } if (part[j] === P_WHEEL) { const mm = machines[mach[j]]; if (mm) mm.flow += PH[mat[j]] === PH_LIQUID ? 1 : 0.5; }
const d = temp[j] - AMB[ny]; const d = temp[j] - AMB[ny];
if (d > 25 || d < -25) wakeHeatXY(nx, ny); if (d > 25 || d < -25) wakeHeatXY(nx, ny);
@@ -55,6 +62,10 @@ function enter(m, ph, j, dx, dy) {
// Heavy grains settle through a loose pile of much lighter ones, slowly. A sorting // Heavy grains settle through a loose pile of much lighter ones, slowly. A sorting
// step keeps its area awake for a while so a pile finishes sorting even at rest. // step keeps its area awake for a while so a pile finishes sorting even at rest.
let SORTP = 0.003; let SORTP = 0.003;
// Liquids step up to LSTEP cells toward a drop in one tick (fewer the thicker they are), and a top-layer grain looks LFILM
// times as far for one (a two-times range strands the far end of a 200-grain-wide ledge).
// World generation settles with both at 1, so every seed keeps making the same world.
let LSTEP = 8, LFILM = 4;
function powder(i, x, y, m) { function powder(i, x, y, m) {
if (y >= H - 1) return; if (y >= H - 1) return;
const b = i + W; const b = i + W;
@@ -97,7 +108,7 @@ function liquid(i, x, y, m) {
if (nx >= 0 && nx < W && enter(m, PH_LIQUID, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; } if (nx >= 0 && nx < W && enter(m, PH_LIQUID, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
} }
if (rnd() > FLUID[m]) return; if (rnd() > FLUID[m]) return;
const sp = SPREAD[m]; const sp = SPREAD[m] * (y === 0 || PH[mat[i - W]] !== PH_LIQUID ? LFILM : 1);
const d0 = (RS & 4) ? 1 : -1; const d0 = (RS & 4) ? 1 : -1;
for (let pass = 0; pass < 2; pass++) { for (let pass = 0; pass < 2; pass++) {
const d = pass ? -d0 : d0; const d = pass ? -d0 : d0;
@@ -107,8 +118,8 @@ function liquid(i, x, y, m) {
const j = i + d * s; const j = i + d * s;
if (!passable(m, PH_LIQUID, j, d, 0)) break; if (!passable(m, PH_LIQUID, j, d, 0)) break;
if (y < H - 1 && passable(m, PH_LIQUID, j + W, 0, 1)) { if (y < H - 1 && passable(m, PH_LIQUID, j + W, 0, 1)) {
// step one cell toward the drop (keeps flow continuous and local) const k = Math.min(s, Math.max(1, (LSTEP * FLUID[m]) | 0)); // thick liquids take shorter steps
move(i, i + d, x, y, x + d, y); move(i, i + d * k, x, y, x + d * k, y);
return; return;
} }
} }
@@ -127,7 +138,7 @@ function gas(i, x, y, m) {
if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; } if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; }
life[i] = l - 1; life[i] = l - 1;
} }
} } else if (m === STEAM && temp[i] < CONDT[STEAM]) wakeHeatXY(x, y); // cooled steam drifting under a roof needs the heat pass to condense it
if (y === 0 || (m === STEAM && y <= weather.skyY + 4 && y < roofY[x])) { if (y === 0 || (m === STEAM && y <= weather.skyY + 4 && y < roofY[x])) {
// steam that has risen to cloud height in the open sky joins the clouds // steam that has risen to cloud height in the open sky joins the clouds
if (m === STEAM) absorbSteam(x); if (m === STEAM) absorbSteam(x);
+6 -4
View File
@@ -71,15 +71,17 @@ function weatherTick() {
if (sh.left !== undefined) { sh.left -= made; if (sh.left <= 0 && sh.until > tick) sh.until = tick; } // made up the shortfall: it passes if (sh.left !== undefined) { sh.left -= made; if (sh.left <= 0 && sh.until > tick) sh.until = tick; } // made up the shortfall: it passes
} }
} }
// clouds made of your steam build up, then rain it back out where they hang // clouds made of your steam build up, then rain it back out over where they hang and a band
// either side, so water boiled off one spot does not all fall back on it
for (let b = 0; b < n; b++) { for (let b = 0; b < n; b++) {
if (!w.raining[b] && w.moist[b] > 150) w.raining[b] = 1; if (!w.raining[b] && w.moist[b] > 150) w.raining[b] = 1;
if (w.raining[b]) { w.moist[b] -= spawnRain(b * WBAND, (b + 1) * WBAND, Math.min(3, 0.5 + w.moist[b] / 100)); if (w.moist[b] < 12) w.raining[b] = 0; } if (w.raining[b]) { w.moist[b] -= spawnRain(Math.max(0, (b - 1) * WBAND), Math.min(W, (b + 2) * WBAND), Math.min(3, 0.5 + w.moist[b] / 100)); if (w.moist[b] < 12) w.raining[b] = 0; }
} }
// and drift a little downwind every second; at the edge of the world they pile up instead // and drift a little downwind every second, raining ones at half the pace; at the edge of the
// world they pile up instead
if (tick % 60 === 0) { if (tick % 60 === 0) {
const out = new Float32Array(n), d = w.wind > 0 ? 1 : -1, f = Math.min(0.12, Math.abs(w.wind) * 0.1); const out = new Float32Array(n), d = w.wind > 0 ? 1 : -1, f = Math.min(0.12, Math.abs(w.wind) * 0.1);
for (let b = 0; b < n; b++) { const mv = w.raining[b] ? 0 : w.moist[b] * f; out[b] += w.moist[b] - mv; if (b + d >= 0 && b + d < n) out[b + d] += mv; else out[b] += mv; } for (let b = 0; b < n; b++) { const mv = w.moist[b] * f * (w.raining[b] ? 0.5 : 1); out[b] += w.moist[b] - mv; if (b + d >= 0 && b + d < n) out[b + d] += mv; else out[b] += mv; }
w.moist = out; w.moist = out;
} }
} }
+3 -1
View File
@@ -303,8 +303,9 @@ function* worldSteps(seedIn, w, h) {
} }
fillNoise(); fillNoise();
yield [0.75, 'Settling loose ground']; yield [0.75, 'Settling loose ground'];
// settle loose material // settle loose material, with water at the speed it had when the seeds were made
awake.fill(3); awake.fill(3);
const lstep = LSTEP, lfilm = LFILM; LSTEP = LFILM = 1;
for (let k = 0; k < 260; k++) { for (let k = 0; k < 260; k++) {
tick++; tickStamp = (tickStamp % 255) + 1; tick++; tickStamp = (tickStamp % 255) + 1;
simMove(); simMove();
@@ -312,6 +313,7 @@ function* worldSteps(seedIn, w, h) {
if (!any) break; if (!any) break;
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground']; if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
} }
LSTEP = lstep; LFILM = lfilm;
yield [0.99, 'Finishing']; yield [0.99, 'Finishing'];
// re-ambient after settling, then start clean // 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; } 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; }
+2 -1
View File
@@ -24,7 +24,8 @@ ticks(run, 900);
const wOut = count(run, run('WATER'), 124, 0, 319, 119), sOut = count(run, run('SAND'), 124, 0, 319, 119); const wOut = count(run, run('WATER'), 124, 0, 319, 119), sOut = count(run, run('SAND'), 124, 0, 319, 119);
check('a lift standing in a flooded pit brings up the sand but not the water', sOut > 150 && wOut === 0, `sand out ${sOut}, water out ${wOut}, water standing in the tubes ${tubeLiquid()}`); check('a lift standing in a flooded pit brings up the sand but not the water', sOut > 150 && wOut === 0, `sand out ${sOut}, water out ${wOut}, water standing in the tubes ${tubeLiquid()}`);
const pitClouds = run('weather.moist.reduce((a, v) => a + v, 0)'); const pitClouds = run('weather.moist.reduce((a, v) => a + v, 0)');
check('the pit keeps its water (bar what steams off into the clouds)', water0 - count(run, run('WATER')) - pitClouds <= 6, `${water0} -> ${count(run, run('WATER'))}, ${pitClouds.toFixed(0)} in the clouds`); const pitSteam = count(run, run('STEAM')); // the pit's floor is hot: some is steam on its way up at any moment
check('the pit keeps its water (bar what steams off into the clouds)', water0 - count(run, run('WATER')) - pitSteam - pitClouds <= 6, `${water0} -> ${count(run, run('WATER'))}, ${pitSteam} steam, ${pitClouds.toFixed(0)} in the clouds`);
fresh(); fresh();
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_TUBE, 1, bx, 25, true);'); run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_TUBE, 1, bx, 25, true);');
rect(run, 84, 60, 95, 79, run('WATER')); rect(run, 84, 60, 95, 79, run('WATER'));
+3 -2
View File
@@ -94,8 +94,9 @@ run('for (let x = 100; x < 104; x++) setCell(98 * W + x, ASH, 1320, 0); for (let
run('for (let k = 0; k < 120; k++) { for (let y = 90; y < 100; y++) for (let x = 96; x < 108; x++) if (mat[y * W + x] === ASH) temp[y * W + x] = Math.max(temp[y * W + x], 1320); simTick(); }'); run('for (let k = 0; k < 120; k++) { for (let y = 90; y < 100; y++) for (let x = 96; x < 108; x++) if (mat[y * W + x] === ASH) temp[y * W + x] = Math.max(temp[y * W + x], 1320); simTick(); }');
check('ash fuses into slag above 1250°C', count(run, run('ASH'), 96, 90, 107, 99) === 0 && count(run, run('MSLAG')) + count(run, run('SLAG')) === 4, `ash ${count(run, run('ASH'), 96, 90, 107, 99)}, slag ${count(run, run('MSLAG')) + count(run, run('SLAG'))}`); check('ash fuses into slag above 1250°C', count(run, run('ASH'), 96, 90, 107, 99) === 0 && count(run, run('MSLAG')) + count(run, run('SLAG')) === 4, `ash ${count(run, run('ASH'), 96, 90, 107, 99)}, slag ${count(run, run('MSLAG')) + count(run, run('SLAG'))}`);
check('ash in a cooler fire stays ash', count(run, run('ASH'), 108, 90, 116, 99) === 4); check('ash in a cooler fire stays ash', count(run, run('ASH'), 108, 90, 116, 99) === 4);
// a mixed charge stays mixed until it burns; ore then works its way down through the burning bed // a mixed charge stays mixed until it burns; ore then works its way down through the burning bed.
fresh(); // The sinking is random: seed it so the result does not hang on what earlier tests drew.
fresh(); run('RS = 7919;');
rect(run, 100, 90, 101, 99, run('STONE')); rect(run, 114, 90, 115, 99, run('STONE')); rect(run, 100, 90, 101, 99, run('STONE')); rect(run, 114, 90, 115, 99, run('STONE'));
run('{ const r = mulberry32(3); for (let y = 90; y <= 99; y++) for (let x = 102; x <= 113; x++) setCell(y * W + x, r() < 0.5 ? CUORE : COAL, AMB[y], 0); }'); run('{ const r = mulberry32(3); for (let y = 90; y <= 99; y++) for (let x = 102; x <= 113; x++) setCell(y * W + x, r() < 0.5 ? CUORE : COAL, AMB[y], 0); }');
const oreY = () => run('(()=>{let s=0,n=0;for(let i=0;i<N;i++) if(mat[i]===CUORE){s+=(i/W)|0;n++;} return s/n})()'); const oreY = () => run('(()=>{let s=0,n=0;for(let i=0;i<N;i++) if(mat[i]===CUORE){s+=(i/W)|0;n++;} return s/n})()');
+116
View File
@@ -0,0 +1,116 @@
// Water conservation, settling, ledge pouring, boundaries, pipes and phase changes.
// Tall worlds keep geothermal heat out of the ponds.
const { load, suite, flatWorld, put, rect, count, ticks } = require('./harness');
const { run } = load();
const { check, done } = suite();
const started = Date.now();
const water = () => run('worldWater()');
const awake = () => run('awake.reduce((n, v) => n + (v > 0), 0)');
const grains = m => count(run, run(m));
function world(w = 320, ground = 120) {
flatWorld(run, w, 600, ground);
run('RS = 12345;');
}
function basin(x0, x1, top = 90) {
rect(run, x0 - 1, top, x0 - 1, 119, run('STONE'));
rect(run, x1 + 1, top, x1 + 1, 119, run('STONE'));
}
world(160); basin(40, 79);
rect(run, 40, 100, 79, 119, run('WATER'));
const closed = water(); ticks(run, 36000);
check('a stone pond keeps every grain for ten minutes and sleeps', water() === closed && awake() === 0, `${closed} -> ${water()}, ${awake()} awake areas; old: 800 water, 0 awake`);
world(240);
rect(run, 115, 60, 124, 99, run('WATER')); ticks(run, 3600);
const heights = run(`(() => { const a = []; for (let x = 0; x < W; x++) {
let y = 120; for (let j = 0; j < 120; j++) if (mat[j*W+x] === WATER) { y = j; break; }
a.push(y);
} return a; })()`);
const uneven = Math.max(...heights) - Math.min(...heights);
check('a falling column spreads into a flat sleeping pool', uneven <= 2 && awake() === 0 && water() === 400, `surface difference ${uneven}, ${awake()} awake, ${water()}/400 grains; old: difference 4, 0 awake, 400 water`);
// Old 2400-tick counts: 24 (40x20), 900 (100x20), 1440 (200x10), 0 (middle).
// Old supplied 7200-tick counts: 254 left for 100x20, 1027 for 200x10.
// Old code eventually sleeps with 24, 171 and 645 grains left, even at 100080 ticks.
// Deadlines below allow the new flow to reach <= 2% and <= 20 grains.
for (const [width, depth, middle] of [[40, 20, false], [100, 20, false], [200, 10, false], [40, 20, true]]) {
world();
const left = middle ? 140 : 0, right = left + width - 1;
rect(run, right + 1, 120, 319, 199, run('EMPTY'));
if (middle) rect(run, 0, 120, left - 1, 199, run('EMPTY'));
rect(run, left, 120 - depth, right, 119, run('WATER'));
const deadline = width === 200 ? 13200 : width === 100 ? 4500 : 900;
const before = water(); ticks(run, deadline);
const remaining = count(run, run('WATER'), left, 0, right, 119);
check(`${width}-wide pool pours off ${middle ? 'both sides' : 'the side ledge'} promptly`, remaining <= Math.min(20, before * 0.02) && water() === before, `${remaining}/${before} left at ${deadline} ticks, total ${water()}; old at same deadline: ${middle ? 0 : width === 40 ? 142 : width === 100 ? 524 : 706}`);
}
for (const edge of [0, 319]) {
world();
const left = edge === 0 ? 0 : 310, right = left + 9;
rect(run, edge === 0 ? 10 : 309, 90, edge === 0 ? 10 : 309, 119, run('STONE'));
rect(run, left, 100, right, 119, run('WATER')); ticks(run, 1800);
const local = count(run, run('WATER'), left, 90, right, 119);
check(`water rests against the ${edge === 0 ? 'left' : 'right'} world wall without wrapping or leaking`, local === 200 && grains('WATER') === 200 && water() === 200 && awake() === 0, `${local}/200 inside, ${grains('WATER')} worldwide, ${awake()} awake; old: 200 inside/worldwide, 0 awake`);
}
for (const y of [0, 599]) {
world(80, 600);
// Isolate movement from geothermal boiling at the bottom boundary.
run('AMB.fill(20); temp.fill(20);');
rect(run, 30, y, 39, y, run('WATER')); ticks(run, 1800);
check(`water dropped on the ${y === 0 ? 'top' : 'bottom'} row settles inside the world`, water() === 10 && count(run, run('WATER'), 0, 599, 79, 599) === 10 && awake() === 0, `${water()}/10 grains, ${awake()} awake; old: 10 water, 0 awake`);
}
for (const m of ['WATER', 'SAND', 'STEAM']) {
world(80, 600); run('AMB.fill(20); temp.fill(20);');
for (const x of [0, 79]) for (const y of [0, 599]) put(run, x, y, run(m));
ticks(run, 600);
check(`${m.toLowerCase()} at all four corners runs without throwing`, m !== 'WATER' || water() === 4, `${grains(m)} grains remain, water total ${water()}; old: ${m === 'STEAM' ? 0 : 4} grains, water total ${m === 'SAND' ? 0 : 4}`);
}
// t_pipes already checks line transport, conservation, valves and full pipes.
function pumpRig(sealed) {
world(240); basin(40, 71, 80);
rect(run, 40, 90, 71, 119, run('WATER'));
run(`placeBlock(BP_PUMP, 1, 17, 29, true);
for (let by = 19; by < 29; by++) placeBlock(BP_PIPE, 0, 17, by, true);
for (let bx = 18; bx < 30; bx++) placeBlock(BP_PIPE, 0, bx, 19, true);
placeBlock(BP_OUTLET, 0, 30, 19, true);`);
basin(120, sealed ? 123 : 159, 80);
run('updateRoof(); rebuildPipes();');
}
pumpRig(false);
const pumped = water(); ticks(run, 6000);
// The last 25 bottom-row grains are beyond the intake (x65..67); no drop draws them closer.
const unreachable = count(run, run('WATER'), 40, 80, 71, 119);
check('a pump drains a pond down to its intake', unreachable === 25 && count(run, run('WATER'), 40, 119, 64, 119) === 25 && water() === pumped, `${unreachable} unreachable, total ${pumped} -> ${water()}; old: 25 unreachable, 960 total`);
pumpRig(true);
const filling = water(); ticks(run, 2400);
const full = count(run, run('WATER'), 120, 80, 123, 119), held = run('pipeNets[0].n');
ticks(run, 600);
check('a full closed basin stops its outlet without losing water', full === 160 && count(run, run('WATER'), 120, 80, 123, 119) === full && run('pipeNets[0].n') === held && water() === filling, `${full}/160 in basin, line ${held} -> ${run('pipeNets[0].n')}, total ${filling} -> ${water()}; old: 160 basin, 384 line, 960 total`);
world(160); basin(40, 79);
run('for (let y = 60; y < 140; y++) AMB[y] = -6;');
rect(run, 40, 110, 79, 119, run('WATER'));
const frozen = water(); ticks(run, 6000);
check('a pond in freezing air ices over without losing water', grains('ICE') >= 40 && water() === frozen, `${grains('ICE')} ice, ${grains('WATER')} water, total ${water()}/${frozen}; old: 400 ice, 0 water, 400 total`);
run('AMB.fill(20); for (let y = 60; y < 140; y++) for (let x = 39; x <= 80; x++) wakeHeatXY(x, y);');
ticks(run, 6000);
check('a thawed pond returns all its water', grains('WATER') === frozen && water() === frozen, `${grains('WATER')}/${frozen} water, ${grains('ICE')} ice; old: 400 water, 0 ice`);
world(160); basin(40, 79, 80);
rect(run, 39, 79, 80, 79, run('STONE'));
run('for (let y = 116; y < 120; y++) for (let x = 40; x < 80; x++) setCell(y*W+x, COPPER, 600, 0);');
rect(run, 40, 115, 79, 115, run('WATER')); run('updateRoof();');
const boiling = water(); let peakSteam = 0;
for (let k = 0; k < 120; k++) { ticks(run, 5); peakSteam = Math.max(peakSteam, grains('STEAM')); }
check('water on hot copper boils into steam', peakSteam > 0 && water() === boiling, `peak ${peakSteam} steam, total ${water()}/${boiling}; old: peak 20 steam, 40 total`);
ticks(run, 24000);
check('steam under a roof returns to water without escaping', grains('WATER') === boiling && water() === boiling && grains('STEAM') === 0, `${grains('WATER')}/${boiling} water, ${grains('STEAM')} steam; old: 37 water, 3 steam`);
// a pool pouring into a shaft down to rock above 200°C: the water boils off and goes into the
// clouds, which spread it out downwind, so the pool really drains and no water is made or lost
flatWorld(run, 640, 400, 120);
const hotY = run('(() => { for (let y = 120; y < H; y++) if (AMB[y] > 200) return y; return -1; })()');
rect(run, 60, 100, 61, 119, run('STONE')); rect(run, 62, 105, 159, 119, run('WATER'));
rect(run, 160, 120, 163, hotY + 20, run('EMPTY'));
run(`for (let y = ${hotY}; y <= ${hotY + 21}; y++) for (let x = 159; x <= 164; x++) if (mat[y*W+x] === STONE) temp[y*W+x] = AMB[y]; weather.wind = 0.3; updateRoof();`);
const shaftPool = () => count(run, run('WATER'), 40, 0, 220, 119);
const shaftWater = water(), shaftPool0 = shaftPool();
ticks(run, 20000);
check('a pool pouring into a hot shaft drains away and no water is made or lost', water() === shaftWater && shaftPool() < shaftPool0 * 0.4, `pool ${shaftPool0} -> ${shaftPool()}, ${Math.round(run('weather.moist.reduce((a, v) => a + v, 0)'))} in the clouds, total ${shaftWater} -> ${water()}`);
console.log(`Water suite time: ${Date.now() - started} ms`);
done();
+12 -4
View File
@@ -27,16 +27,24 @@ rect(run, 150, 100, 169, 109, run('STEAM'));
run('updateRoof(); for (let y = 100; y < 110; y++) for (let x = 150; x < 170; x++) temp[y*W+x] = 60;'); run('updateRoof(); for (let y = 100; y < 110; y++) for (let x = 150; x < 170; x++) temp[y*W+x] = 60;');
ticks(run, 600); ticks(run, 600);
check('under a roof steam still turns back into water', count(run, run('WATER')) > 120 && moist() === 0, `water ${count(run, run('WATER'))}, clouds ${moist()}`); check('under a roof steam still turns back into water', count(run, run('WATER')) > 120 && moist() === 0, `water ${count(run, run('WATER'))}, clouds ${moist()}`);
// clouds rain back out over the band where they gathered // clouds rain back out over the band where they gathered, and a band either side
flatWorld(run, 512, 200, 120); flatWorld(run, 512, 200, 120);
run('weather.moist[3] = 400; weather.wind = 0;'); run('weather.moist[3] = 400; weather.wind = 0;');
ticks(run, 2); ticks(run, 2);
check('a big cloud starts to rain', run('weather.raining[3]') === 1); check('a big cloud starts to rain', run('weather.raining[3]') === 1);
ticks(run, 900); let drops = 0, near = 0; // where the drops fall (the water spreads out over the flat ground after it lands)
for (let k = 0; k < 90; k++) { ticks(run, 10); for (const x of JSON.parse(run('JSON.stringify(flyers.filter(f => f.rain).map(f => f.x | 0))'))) { drops++; if (x >= 2 * 64 && x < 5 * 64) near++; } }
const landed = count(run, run('WATER')); const landed = count(run, run('WATER'));
const near = count(run, run('WATER'), 3 * 64 - 64, 0, 4 * 64 + 64, 199);
check('a cloud rains its water back down', landed + rainInAir(run('WATER')) + moist() > 390 && landed > 250, `landed ${landed}, in the air ${rainInAir(run('WATER'))}, left in the cloud ${moist().toFixed(0)}`); check('a cloud rains its water back down', landed + rainInAir(run('WATER')) + moist() > 390 && landed > 250, `landed ${landed}, in the air ${rainInAir(run('WATER'))}, left in the cloud ${moist().toFixed(0)}`);
check('close to where it gathered', near > landed * 0.85, `${near} of ${landed} within a band`); check('it falls near where it gathered, give or take a band', drops > 0 && near > drops * 0.85, `${near} of ${drops} drops seen within a band`);
// with a wind the raining cloud keeps drifting, so the rain is spread out downwind instead of all
// falling back where the steam rose (a waterfall into a hot shaft would otherwise refill its own lake)
flatWorld(run, 1024, 200, 120);
run('weather.moist[3] = 400; weather.wind = 0.3;');
const fell = new Array(16).fill(0);
for (let k = 0; k < 150; k++) { ticks(run, 10); for (const x of JSON.parse(run('JSON.stringify(flyers.filter(f => f.rain).map(f => f.x | 0))'))) fell[(x / 64) | 0]++; }
const allRain = fell.reduce((a, v) => a + v, 0), downwind = fell.slice(2, 8).reduce((a, v) => a + v, 0);
check('in a wind the rain spreads out downwind of the cloud', allRain > 0 && downwind > allRain * 0.85 && fell[3] < allRain * 0.6, `by band ${fell.join(' ')}`);
// boiling water outdoors comes back as rain // boiling water outdoors comes back as rain
flatWorld(run, 320, 200, 120); flatWorld(run, 320, 200, 120);
rect(run, 140, 116, 179, 119, run('WATER')); rect(run, 140, 116, 179, 119, run('WATER'));