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