Files
deepgrain-foundry/test/t_pipes.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

102 lines
7.4 KiB
JavaScript

// Pipes, pumps and outlets move liquids; the Refiner grinds waste rock for trace ore.
const { load, suite, flatWorld, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
const box = (x0, y0, x1, y1, m, fl = 'F_NAT') => run(`for (let y = ${y0}; y <= ${y1}; y++) for (let x = ${x0}; x <= ${x1}; x++) setCell(y * W + x, ${m}, AMB[y], ${fl})`);
const lineWater = () => run('pipeNets.reduce((a, n) => a + (n.m === WATER ? n.n : 0), 0)');
const totalWater = () => count(run, run('WATER')) + lineWater();
// two stone basins; a pump in the bottom corner of the left one pushes water up a pipe, over the
// wall and out of an outlet into the right one
function rig() {
flatWorld(run, 240, 160, 100);
run('money = 5000; bot.x = 110; bot.y = 50;');
box(36, 80, 39, 99, 'STONE'); box(72, 80, 75, 99, 'STONE'); // left basin walls
box(108, 80, 111, 99, 'STONE'); box(144, 80, 147, 99, 'STONE'); // right basin walls
box(40, 84, 71, 99, 'WATER', 0);
run(`placeBlock(BP_PUMP, 1, 17, 24, false); // pushes right, draws in from the left
for (let by = 19; by <= 23; by++) placeBlock(BP_PIPE, 0, 17, by, false);
for (let bx = 18; bx <= 28; bx++) placeBlock(BP_PIPE, 0, bx, 19, false);
placeBlock(BP_OUTLET, 0, 29, 19, false); // pours down`);
}
rig();
const w0 = totalWater(), ww0 = run('worldWater()');
run('for (let k = 0; k < 900; k++) simTick();');
const right = count(run, run('WATER'), 112, 60, 143, 99);
check('a pump, a pipe over the wall and an outlet move water from one basin to the other', right > 250, `${right} grains in the right basin, ${lineWater()} in the pipes`);
check('no water is lost or made on the way', totalWater() === w0, `${w0} -> ${totalWater()}`);
check('the weather counts water in pipes, so it sends no rain to make up for it', run('worldWater()') === ww0, `${ww0} -> ${run('worldWater()')}`);
// the line is one network: pump, 16 pipes, outlet
check('touching pipes, pump and outlet make one line', run('(rebuildPipes(), pipeNets.length)') === 1 && run('pipeNets[0].blocks.length') === 18, `${run('pipeNets.length')} lines, ${run('pipeNets[0].blocks.length')} blocks`);
// an outlet closed with F holds the water in the line; the pump stops when the line is full
rig();
run('interactAt(29 * 4 + 1, 19 * 4 + 1); for (let k = 0; k < 900; k++) simTick();');
check('with the outlet closed the line fills up and nothing comes out', count(run, run('WATER'), 112, 60, 143, 99) === 0 && lineWater() === 18 * 16, `${lineWater()} in the line`);
run('interactAt(29 * 4 + 1, 19 * 4 + 1); for (let k = 0; k < 120; k++) simTick();');
check('opened again, it pours', count(run, run('WATER'), 112, 60, 143, 99) > 50);
// taking a pipe back spills its share where it was
rig();
run('interactAt(29 * 4 + 1, 19 * 4 + 1); for (let k = 0; k < 900; k++) simTick();');
const before = totalWater();
run('removeBlock(23, 19); rebuildPipes();');
check('taking a full pipe back spills its 16 grains of water there', totalWater() === before && count(run, run('WATER'), 88, 70, 99, 99) > 0, `${before} -> ${totalWater()}`);
check('and the line is cut in two', run('pipeNets.length') === 2, run('pipeNets.length'));
// one liquid per line, and nothing hotter than 500°C
rig();
run('interactAt(29 * 4 + 1, 19 * 4 + 1); for (let k = 0; k < 60; k++) simTick();'); // outlet closed: some water stays in the line
box(40, 84, 71, 99, 'EMPTY', 0); box(40, 90, 71, 99, 'MSN', 0); run('for (let y = 90; y < 100; y++) for (let x = 40; x < 72; x++) temp[y * W + x] = 300;');
run('for (let k = 0; k < 120; k++) simTick();');
check('a line holding water will not take molten tin', count(run, run('MSN')) + count(run, run('TIN')) === 32 * 10 && run('pipeNets[0].m') === run('WATER'), `${count(run, run('MSN')) + count(run, run('TIN'))} tin left, line holds ${run('MD[pipeNets[0].m].name')}`);
rig();
box(40, 84, 71, 99, 'EMPTY', 0); box(40, 90, 71, 99, 'MCU', 0); run('for (let y = 90; y < 100; y++) for (let x = 40; x < 72; x++) temp[y * W + x] = 1200;');
run('for (let k = 0; k < 60; k++) { for (let y = 90; y < 100; y++) for (let x = 40; x < 72; x++) if (mat[y * W + x] === MCU) temp[y * W + x] = 1200; simTick(); }');
check('a pump will not take molten copper at 1200°C', run('pipeNets[0].n') === 0, run('pipeNets[0].n'));
// what is in the pipes is saved
rig();
run('interactAt(29 * 4 + 1, 19 * 4 + 1); for (let k = 0; k < 400; k++) simTick();');
const held = lineWater();
run('(() => { const s = serializeWorld(); deserializeWorld(JSON.parse(JSON.stringify(s.meta)), s.buffers); })()');
check('the water in a line survives saving and loading', held > 100 && lineWater() === held, `${held} -> ${lineWater()}`);
// pipes are thin: water and sand pass through a pipe run instead of being held back by it
flatWorld(run, 240, 160, 100);
run('money = 5000; bot.x = 60; bot.y = 50;');
box(36, 76, 39, 99, 'STONE'); box(88, 76, 91, 99, 'STONE');
run('for (let by = 19; by <= 24; by++) placeBlock(BP_PIPE, 0, 15, by, false);'); // a pipe wall at x 60..63
box(40, 80, 59, 99, 'WATER', 0);
run('for (let k = 0; k < 400; k++) simTick();');
check('water flows through a run of pipe', count(run, run('WATER'), 64, 70, 87, 99) > 60, `${count(run, run('WATER'), 64, 70, 87, 99)} grains got past`);
box(60, 64, 63, 70, 'SAND', 0); run('for (let k = 0; k < 300; k++) simTick();');
check('and sand dropped on a pipe falls down through it', count(run, run('SAND'), 40, 80, 87, 99) > 20, `${count(run, run('SAND'), 40, 80, 87, 99)} of 28 at the bottom`);
// ---- the Refiner
function refinerRig(feed, n) {
flatWorld(run, 240, 160, 100);
run(`money = 5000; bot.x = 110; bot.y = 40; stats.refined = 0; stats.refinedOre = 0; placeMachineAt('refiner', 0, 26, 15);`); // x 104..115, y 60..71; chute out at y 72
run(`{ let k = 0; for (let y = 60; y < 70 && k < ${n}; y++) for (let x = 106; x < 114 && k < ${n}; x++, k++) setCell(y * W + x, ${feed}, AMB[y], 0); }`);
}
refinerRig('RSTONE', 80);
const ore = () => count(run, run('CUORE')) + count(run, run('COAL')) + count(run, run('FEORE')) + count(run, run('SNORE'));
// five hopper-loads, each ground before the next goes in
for (let k = 0; k < 5; k++) { run('for (let k = 0; k < 300; k++) simTick();'); if (k < 4) run('for (let y = 60; y < 70; y++) for (let x = 106; x < 114; x++) setCell(y * W + x, RSTONE, AMB[y], 0);'); }
const fed = 400, sand = count(run, run('SAND')), o = ore();
check('the Refiner grinds all the stone rubble it is fed', count(run, run('RSTONE')) === 0 && run('stats.refined') === fed, `${count(run, run('RSTONE'))} left, ${run('stats.refined')} of ${fed} ground`);
check('about half as much sand comes out', sand > fed * 0.35 && sand < fed * 0.6, `${sand} sand from ${fed} rubble`);
check('with the odd grain of common ore', o > 0 && o < fed * 0.15, `${o} ore grains`);
// gravel, sand and dirt are refined too; ore drops straight through
refinerRig('LDIRT', 60); run('for (let k = 0; k < 600; k++) simTick();');
check('dirt goes through the refiner too', count(run, run('LDIRT')) === 0 && run('stats.refined') === 60);
refinerRig('CUORE', 30); run('for (let k = 0; k < 400; k++) simTick();');
check('ore it cannot grind drops straight through, all of it', count(run, run('CUORE')) === 30 && count(run, run('CUORE'), 104, 60, 115, 71) === 0, count(run, run('CUORE'), 104, 60, 115, 71));
// a blocked chute stops it
refinerRig('RSTONE', 40);
box(100, 72, 120, 73, 'STONE');
run('for (let k = 0; k < 200; k++) simTick();');
check('with the space under its chute blocked it stops, and says so', count(run, run('RSTONE')) === 40 && run("machines.find(m => m && m.type === 'refiner').blocked") === true);
done();