From 9291040ff0d3f6926cfec86d42e32856df7a1db3 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Sat, 24 Oct 2026 20:44:06 -0400 Subject: [PATCH] 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 --- docs/DESIGN.md | 13 +++++ docs/PROJECT_MAP.md | 9 ++-- docs/agent-log.md | 20 ++++++++ src/sim/cells.js | 19 ++++++-- src/sim/weather.js | 10 ++-- src/sim/worldgen.js | 4 +- test/t_conveyors.js | 3 +- test/t_molds.js | 5 +- test/t_water.js | 116 ++++++++++++++++++++++++++++++++++++++++++++ test/t_weather.js | 16 ++++-- 10 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 test/t_water.js diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 3b680e2..f7d0930 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -338,6 +338,19 @@ above, including: version's narrow fade too sharp. 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 - **Concrete**, as a stronger wall material from rubble. diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 6946a95..b1588b6 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -226,10 +226,10 @@ Exploration only: never changes grains or the random stream. - functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll - 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). - functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove -- variables: SORTP +- variables: SORTP, LSTEP, LFILM ### src/sim/heat.js (246 lines) 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. - 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. - functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick - 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. - 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. - functions: makeNoise, fbm, clearHardSpecks, generateWorld, generateWorldAsync - 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_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_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_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. diff --git a/docs/agent-log.md b/docs/agent-log.md index c05b9d7..1c72116 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -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-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) - `232f5af`: `entTick` snap-and-tip (`entSupportCount`, `src/sim/items.js`); diff --git a/src/sim/cells.js b/src/sim/cells.js index 7b3b68c..78e9852 100644 --- a/src/sim/cells.js +++ b/src/sim/cells.js @@ -21,6 +21,13 @@ function swapCells(i, j) { function move(i, j, x, y, nx, ny) { swapCells(i, j); 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; } const d = temp[j] - AMB[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 // step keeps its area awake for a while so a pile finishes sorting even at rest. 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) { if (y >= H - 1) return; 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 (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; for (let pass = 0; pass < 2; pass++) { const d = pass ? -d0 : d0; @@ -107,8 +118,8 @@ function liquid(i, x, y, m) { const j = i + d * s; if (!passable(m, PH_LIQUID, j, d, 0)) break; if (y < H - 1 && passable(m, PH_LIQUID, j + W, 0, 1)) { - // step one cell toward the drop (keeps flow continuous and local) - move(i, i + d, x, y, x + d, y); + const k = Math.min(s, Math.max(1, (LSTEP * FLUID[m]) | 0)); // thick liquids take shorter steps + move(i, i + d * k, x, y, x + d * k, y); return; } } @@ -127,7 +138,7 @@ function gas(i, x, y, m) { if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; } 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])) { // steam that has risen to cloud height in the open sky joins the clouds if (m === STEAM) absorbSteam(x); diff --git a/src/sim/weather.js b/src/sim/weather.js index d6e6bab..d10ee84 100644 --- a/src/sim/weather.js +++ b/src/sim/weather.js @@ -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 } } - // 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++) { 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) { 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; } } diff --git a/src/sim/worldgen.js b/src/sim/worldgen.js index b99cd61..ecebb30 100644 --- a/src/sim/worldgen.js +++ b/src/sim/worldgen.js @@ -303,8 +303,9 @@ function* worldSteps(seedIn, w, h) { } fillNoise(); 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); + const lstep = LSTEP, lfilm = LFILM; LSTEP = LFILM = 1; for (let k = 0; k < 260; k++) { tick++; tickStamp = (tickStamp % 255) + 1; simMove(); @@ -312,6 +313,7 @@ function* worldSteps(seedIn, w, h) { if (!any) break; if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground']; } + LSTEP = lstep; LFILM = lfilm; yield [0.99, 'Finishing']; // 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; } diff --git a/test/t_conveyors.js b/test/t_conveyors.js index f604315..5110670 100644 --- a/test/t_conveyors.js +++ b/test/t_conveyors.js @@ -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); 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)'); -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(); run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_TUBE, 1, bx, 25, true);'); rect(run, 84, 60, 95, 79, run('WATER')); diff --git a/test/t_molds.js b/test/t_molds.js index 6260da3..ae12e1c 100644 --- a/test/t_molds.js +++ b/test/t_molds.js @@ -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(); }'); 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); -// a mixed charge stays mixed until it burns; ore then works its way down through the burning bed -fresh(); +// a mixed charge stays mixed until it burns; ore then works its way down through the burning bed. +// 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')); 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 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(); diff --git a/test/t_weather.js b/test/t_weather.js index 35a339e..de2f23c 100644 --- a/test/t_weather.js +++ b/test/t_weather.js @@ -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;'); 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()}`); -// 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); run('weather.moist[3] = 400; weather.wind = 0;'); ticks(run, 2); 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 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('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 flatWorld(run, 320, 200, 120); rect(run, 140, 116, 179, 119, run('WATER'));