Files
deepgrain-foundry/test/t_weather.js
T
44r0n7andClaude Opus 5.5 b270c4e818 Deepgrain Foundry: move from a claude.ai artifact to a local repository
The game as of artifact version 24, ready for local development with
coding agents.

- Source split from three large files into 52 small ones under src/sim,
  src/render and src/ui, joined in the order in src/manifest.json by
  build.py. The joined code is unchanged apart from comments; every
  headless suite gives the same output, and screenshots in both looks
  are pixel-identical to before.
- Light tidying with no change in behaviour: one movement scan loop,
  shared hazard-stripe and belt shader helpers, unused code removed, a
  shared check helper for the test suites.
- World files can be exported as an ordinary download outside claude.ai.
- Tests: broken old scripts removed, experiments moved to tools/,
  test/run_all.py added. Tools: shot.py (repeatable screenshots),
  where.py, map.py.
- Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox),
  docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the
  owner's requests and an agent log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
2026-10-23 16:05:31 -04:00

105 lines
8.0 KiB
JavaScript

// Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
const moist = () => run('weather.moist.reduce((a, v) => a + v, 0)');
const rainInAir = (m) => run(`flyers.filter(f => f.rain && f.m === ${m}).length`);
run('generateWorld(777, 1024, 640, null)');
check('new worlds have no springs', count(run, run('SPRING')) === 0);
check('and the same seed still makes the same land', run('surfY[512]') > 0 && run('!!dock()'));
// an older save with a spring in it: the spring becomes plain rock
run('globalThis.__s = serializeWorld(); __s.buffers.mat[300 * W + 500] = SPRING; deserializeWorld(__s.meta, __s.buffers);');
check('springs in older worlds turn into plain rock', run('mat[300 * W + 500]') === run('STONE') && count(run, run('SPRING')) === 0);
// steam in the open sky rises into the clouds instead of condensing
flatWorld(run, 320, 200, 120);
rect(run, 150, 100, 169, 104, run('STEAM'));
run('for (let y = 100; y < 105; y++) for (let x = 150; x < 170; x++) temp[y*W+x] = 60;'); // already cooler than 97°C
const steam0 = count(run, run('STEAM'));
ticks(run, 240);
check('cool steam under open sky rises to cloud height and joins the clouds', count(run, run('STEAM')) === 0 && count(run, run('WATER')) + rainInAir(run('WATER')) === 0 && moist() > steam0 * 0.9, `water ${count(run, run('WATER'))}, clouds ${moist().toFixed(0)} of ${steam0}`);
check('a small cloud just hangs there', run('weather.raining.every(v => !v)'));
// under a roof it condenses as before
flatWorld(run, 320, 200, 120);
rect(run, 140, 80, 179, 81, run('STONE')); rect(run, 140, 80, 141, 119, run('STONE')); rect(run, 178, 80, 179, 119, run('STONE'));
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
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);
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`);
// boiling water outdoors comes back as rain
flatWorld(run, 320, 200, 120);
rect(run, 140, 116, 179, 119, run('WATER'));
run('for (let y = 116; y < 120; y++) for (let x = 140; x < 180; x++) { temp[y*W+x] = 400; wakeHeatXY(x, y); }');
const w0 = count(run, run('WATER'));
for (let k = 0; k < 40; k++) { run('for (let y = 116; y < 120; y++) for (let x = 140; x < 180; x++) if (mat[y*W+x] === WATER && temp[y*W+x] < 150) { temp[y*W+x] = 400; wakeHeatXY(x, y); }'); ticks(run, 30); }
check('water boiled off outdoors goes up into the clouds', moist() > 30 || rainInAir(run('WATER')) > 0, `clouds ${moist().toFixed(0)}, ${count(run, run('WATER'))} of ${w0} still water`);
run('weather.moist.fill(0); weather.moist[2] = 200;'); // more steam piles in: enough for a downpour
ticks(run, 2400);
check('and once the cloud is big enough it falls back down as rain', run('weather.raining.some(v => v)') || count(run, run('WATER')) > 100,
`water ${count(run, run('WATER'))}, clouds ${moist().toFixed(0)}`);
check('nothing is lost on the way', count(run, run('WATER')) + count(run, run('STEAM')) + moist() + rainInAir(run('WATER')) > w0 * 0.85, `water ${count(run, run('WATER'))}, steam ${count(run, run('STEAM'))}, clouds ${moist().toFixed(0)} of ${w0}`);
// water is never made: with all of its water still there, no shower blows in
flatWorld(run, 640, 200, 120);
rect(run, 100, 110, 400, 119, run('WATER'));
run('weather.base = worldWater(); weather.next = tick; EV.length = 0;');
ticks(run, 2);
check('a world with all its water gets no showers from outside', !run('weather.shower') && run('weather.next - tick') > 3600 * 4, run('weather.next - tick'));
// some water left the world (sold at the dock, say): a shower makes up just that much
const wBefore = run('worldWater()');
run('for (let y = 110; y < 120; y++) for (let x = 100; x < 200; x++) setCell(y * W + x, EMPTY, AMB[y], 0);');
run('weather.next = tick; EV.length = 0;');
ticks(run, 2);
const sh = JSON.parse(run('JSON.stringify(weather.shower)'));
check('a world that has lost water gets a shower, and says so', sh && run('EV.some(e => e.t === "weather")'), JSON.stringify(sh));
run('weather.shower.rate = 2; weather.wind = 0;');
ticks(run, 3600);
const wAfter = run('worldWater()');
check('the shower brings back what was lost, and no more', wAfter >= wBefore * 0.97 && wAfter <= wBefore * 1.03, `${wBefore} -> ${wAfter}`);
check('mostly under the shower', count(run, run('WATER'), Math.floor(sh.x0) - 60, 0, Math.ceil(sh.x1) + 60, 199) > (count(run, run('WATER')) - 2000) * 0.9);
ticks(run, 400);
check('the shower passes and the next check is a few minutes off', !run('weather.shower') && run('weather.next - tick') > 3600 * 4, run('weather.next - tick'));
// clouds that drift to the edge of the world stay there
flatWorld(run, 640, 200, 120);
run('weather.moist[0] = 100; weather.moist[9] = 100; weather.wind = 1;');
ticks(run, 600);
check('clouds blown to the edge of the world are not lost', Math.round(moist()) === 200, moist());
check('the sky reads as raining while it rains, dry after', run('rainAt(5)') === 0);
// where the ground is frozen it snows
flatWorld(run, 320, 200, 120);
run('for (let y = 60; y <= 120; y++) AMB[y] = -6; for (let i = 0; i < N; i++) if (mat[i] === EMPTY) temp[i] = AMB[(i / W) | 0]; weather.moist[2] = 200; weather.wind = 0;');
ticks(run, 900);
check('on freezing ground it snows instead', count(run, run('SNOW')) > 100 && count(run, run('WATER')) < 5, `snow ${count(run, run('SNOW'))}, water ${count(run, run('WATER'))}`);
// saving: weather is kept; rain in the air never ends up in your bag
flatWorld(run, 320, 200, 120);
run('weather.moist[1] = 123; weather.next = tick + 5000; bag.fill(0); bagTotal = 0;');
ticks(run, 2);
run('spawnRain(10, 300, 50);');
run('globalThis.__s = serializeWorld(); deserializeWorld(__s.meta, __s.buffers);');
check('save and load keep the clouds and the forecast', Math.round(run('weather.moist[1]')) === 123 - 0 || run('weather.moist[1]') > 100, run('weather.moist[1]'));
check('rain in the air never ends up in your bag', run('bagTotal') === 0, run('bagTotal'));
check('rain in the air at the save goes back into the clouds', run('weather.moist.reduce((a, v) => a + v, 0)') >= 123 + 40, run('weather.moist.reduce((a, v) => a + v, 0)'));
// snow and water
flatWorld(run, 240, 160, 100);
rect(run, 100, 80, 101, 99, run('STONE')); rect(run, 140, 80, 141, 99, run('STONE')); rect(run, 102, 90, 139, 99, run('WATER'));
check('snow blown out of the bag comes out frozen', run('bagTemp(SNOW)') < 0, run('bagTemp(SNOW)'));
run('bag[SNOW] = 200; bagTotal = 200; bot.x = 116; bot.y = 70; for (let k = 0; k < 200 && bag[SNOW] > 0; k++) { toolBlow(110 + (k * 7) % 24, 86, 1, SNOW, null); simTick(); }');
ticks(run, 900);
check('snow that lands on a warm pond melts into it', count(run, run('SNOW')) < 10 && count(run, run('WATER')) > 38 * 10 + 100, `snow ${count(run, run('SNOW'))}, water ${count(run, run('WATER'))}`);
run('for (let y = 60; y < 120; y++) AMB[y] = -6; for (let y = 90; y < 100; y++) for (let x = 102; x < 140; x++) if (mat[y*W+x] === WATER) temp[y*W+x] = 0.2;');
run('for (let x = 102; x < 140; x++) for (let y = 84; y < 88; y++) setCell(y * W + x, SNOW, -6, 0);');
ticks(run, 2400);
check('on freezing ground, a pond with snow on it ices over', count(run, run('ICE')) > 30, `ice ${count(run, run('ICE'))}, water ${count(run, run('WATER'))}`);
done();