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:
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'));
|
||||||
|
|||||||
Reference in New Issue
Block a user