Files
deepgrain-foundry/test/t_water.js
T
44r0n7andClaude Opus 5.5 ea1dd387b1 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>
2026-10-24 20:44:06 -04:00

117 lines
8.1 KiB
JavaScript

// 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();