Files
deepgrain-foundry/test/t_molds.js
T
44r0n7andClaude Opus 5.5 3f971a94c2 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

115 lines
11 KiB
JavaScript

// Molds and furnaces stay usable: knock-outs leave the mold clean, plugs over the mold are
// cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting,
// and slag lets the fire breathe.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
function fresh() { flatWorld(run, 240, 160, 100); run('bot.x = 110; bot.y = 70; bag.fill(0); bagTotal = 0; for (const e of ents.slice()) removeEnt(e);'); }
const cavCount = (k, m) => run(`molds[${k}].cavs[0].filter(i => mat[i] === ${m}).length`);
// an ingot with 27 grains of copper: 3 bars, and the 3 left over go to the bag, not back in the trough
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;');
run('{ const c = molds[0].cavs[0].slice().sort((a, b) => b - a); for (let k = 0; k < 27; k++) { setCell(c[k], COPPER, 30, F_CAST); } } checkMolds();');
let msg = run('collectMold(molds[0])');
check('knocking out an ingot gives the bars', run('ents.length') === 3 && run('ents.every(e => e.m === BARCU)'), msg);
check('and leaves the trough empty, the scrap in your bag', run('molds[0].cavs[0].every(i => mat[i] === EMPTY)') && run('bag[SCRCU]') === 3, `scrap in bag ${run('bag[SCRCU]')}`);
// a plate of copper set over the trough, and drips frozen in the grate above: knocked loose too
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;');
run('for (let bx = 30; bx < 34; bx++) placeBlock(BP_GRATE, 0, bx, 21, true);');
run('{ const mo = molds[0]; for (let x = mo.x + 1; x < mo.x + 15; x++) setCell(mo.y * W + x, COPPER, 30, F_CAST); for (let x = mo.x + 2; x < mo.x + 10; x++) setCell((mo.y - 1) * W + x, COPPER, 30, F_CAST); } checkMolds();');
check('a mold with only a plate over it reads as empty', run('molds[0].state') === 'empty');
msg = run('collectMold(molds[0])');
check('pressing F knocks the plate and the frozen drips loose', run(`(() => { const mo = molds[0]; for (let y = mo.y - 4; y <= mo.y; y++) for (let x = mo.x; x < mo.x + 16; x++) if (mat[y*W+x] === COPPER) return false; return true; })()`) && run('bag[SCRCU]') === 22, `${msg} | scrap ${run('bag[SCRCU]')}`);
check('the grate itself is still there', run('part[(molds[0].y - 1) * W + molds[0].x + 3]') === run('P_GRATE'));
// slag in an ingot is skimmed off; the bars still come out
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;');
run('{ const c = molds[0].cavs[0].slice().sort((a, b) => b - a); for (let k = 0; k < 16; k++) setCell(c[k], COPPER, 30, F_CAST); for (let k = 16; k < 20; k++) setCell(c[k], SLAG, 30, 0); } checkMolds();');
check('an ingot with slag on top is still ready', run('molds[0].state') === 'ready', run('molds[0].state'));
msg = run('collectMold(molds[0])');
check('the slag is chipped off and thrown away, and two bars come out', run('ents.length') === 2 && run('bag[SLAG]') === 0 && /chipped off/.test(msg) && run('molds[0].cavs[0].every(i => mat[i] === EMPTY)'), msg);
// a bit casting with slag in it is spoiled, but F knocks it out instead of asking you to drill
fresh();
run('bag[RSTONE] = 200; bagTotal = 200; placeMoldAt("bit", 30, 20, RSTONE); bag[RSTONE] = 0; bagTotal = 0;');
run('{ const c = molds[0].cavs[0]; c.forEach((i, k) => setCell(i, k === 3 ? SLAG : COPPER, 30, F_CAST)); } checkMolds();');
check('a bit with slag in it is spoiled', run('molds[0].state') === 'flawed', run('molds[0].flaw'));
msg = run('collectMold(molds[0])');
check('F knocks the spoiled bit out as scrap and the mold is ready again', run('molds[0].cavs[0].every(i => mat[i] === EMPTY)') && run('bag[SCRCU]') === 29 && run('bag[SLAG]') === 0 && run('player.bitTier') === 0, msg);
// a blown grain with nowhere to land never ends up below a grate
fresh();
run('for (let bx = 25; bx < 30; bx++) placeBlock(BP_GRATE, 0, bx, 22, true);'); // grate at y 88..91
rect(run, 70, 66, 149, 87, run('SAND')); // the chamber above the grate is packed full up to y 66
const placed = run('depositNear(CUORE, 20, 108, 86)');
check('a grain that cannot land nearby goes up, never through the grate', placed && count(run, run('CUORE'), 0, 66, 239, 159) === 0 && count(run, run('CUORE'), 0, 0, 239, 65) === 1, `below the pile ${count(run, run('CUORE'), 0, 66, 239, 159)}, above it ${count(run, run('CUORE'), 0, 0, 239, 65)}`);
// slag lets the fire breathe: coal buried under slag still finds air
fresh();
rect(run, 100, 95, 110, 99, run('COAL')); rect(run, 100, 92, 110, 94, run('SLAG'));
check('coal under a layer of slag still gets air', run('hasO2(94 * 0 + 95 * W + 105, 105, 95)'));
rect(run, 100, 92, 110, 94, run('RSTONE'));
check('but not under rubble', !run('hasO2(95 * W + 105, 105, 95)'));
// molten slag trickles down through a charge but is too thick for a grate; molten copper runs through
fresh();
run('for (let bx = 25; bx < 30; bx++) placeBlock(BP_GRATE, 0, bx, 23, true);'); // grate at y 92..95
rect(run, 98, 70, 99, 99, run('STONE')); rect(run, 120, 70, 121, 99, run('STONE'));
run('for (let x = 104; x < 112; x++) { setCell(80 * W + x, MSLAG, 1300, 0); life[80 * W + x] = 255; setCell(81 * W + x, MCU, 1300, 0); life[81 * W + x] = 255; } for (let y = 84; y < 92; y++) for (let x = 100; x < 116; x++) setCell(y * W + x, CUORE, 1300, 0);');
run('for (let y = 76; y < 100; y++) for (let x = 100; x < 116; x++) temp[y * W + x] = 1300;');
ticks(run, 240);
const below = m => run(`(()=>{let n=0;for(let y=96;y<100;y++)for(let x=96;x<120;x++)if(mat[y*W+x]===${m})n++;return n})()`);
const sunk = run('(()=>{let n=0;for(let y=84;y<96;y++)for(let x=96;x<120;x++)if(mat[y*W+x]===MSLAG||mat[y*W+x]===SLAG)n++;return n})()');
check('molten slag sinks down through the ore and stops on the grate', sunk >= 6 && below(run('MSLAG')) + below(run('SLAG')) === 0, `slag in the charge ${sunk}, under the grate ${below(run('MSLAG')) + below(run('SLAG'))}`);
check('molten copper runs through the grate', below(run('MCU')) + below(run('COPPER')) >= 6, below(run('MCU')) + below(run('COPPER')));
// ash fuses into molten slag in a hot fire, but not in a wood fire's heat
fresh();
run('for (let x = 100; x < 104; x++) setCell(98 * W + x, ASH, 1320, 0); for (let x = 110; x < 114; x++) setCell(98 * W + x, ASH, 900, 0);');
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 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
fresh();
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); }');
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 y0 = oreY(); ticks(run, 3600); const y1 = oreY();
check('ore does not sift down through unlit coal', Math.abs(y1 - y0) < 0.3, `${y0.toFixed(2)} -> ${y1.toFixed(2)}`);
run('for (let y = 90; y <= 99; y++) for (let x = 102; x <= 113; x++) { const i = y * W + x; if (mat[i] === COAL) { flags[i] |= F_BURN; life[i] = 250; temp[i] = 900; } wakeXY(x, y); }');
run('for (let k = 0; k < 900; k++) { if (k % 8 === 0) wakeXY(107, 95); simTick(); }'); const y2 = oreY();
check('but sinks into a burning bed', y2 > y1 + 0.3, `${y1.toFixed(2)} -> ${y2.toFixed(2)}`);
// knocking out the mold rakes slag and ash off the grate above it, and leaves the charge
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;');
run('for (let bx = 30; bx < 34; bx++) placeBlock(BP_GRATE, 0, bx, 21, true); for (let bx = 29; bx < 35; bx++) placeBlock(BP_WALL, RSTONE, bx, 17, true);');
run('{ const mo = molds[0]; for (let x = mo.x; x < mo.x + 16; x++) { setCell((mo.y - 5) * W + x, SLAG, 30, 0); setCell((mo.y - 6) * W + x, x & 1 ? ASH : SLAG, 30, 0); setCell((mo.y - 7) * W + x, x & 1 ? CUORE : COAL, 30, 0); setCell(70 * W + x, SLAG, 30, 0); } const c = molds[0].cavs[0]; for (let k = 0; k < 16; k++) setCell(c[k], COPPER, 30, F_CAST); } checkMolds();');
msg = run('collectMold(molds[0])');
check('F rakes the slag and ash off the grate', /raked/.test(msg) && count(run, run('SLAG'), 120, 72, 136, 88) + count(run, run('ASH'), 120, 72, 136, 88) === 0 && run('bag[SLAG] + bag[ASH]') === 0, msg);
check('but leaves the unburnt charge, and what is outside the furnace roof', count(run, run('CUORE'), 120, 72, 136, 88) + count(run, run('COAL'), 120, 72, 136, 88) === 16 && count(run, run('SLAG'), 120, 70, 136, 70) === 16);
// slag floating on top of a part-filled bit is not a flaw: fire again, or knock it out
fresh();
run('bag[RSTONE] = 200; bagTotal = 200; placeMoldAt("bit", 30, 20, RSTONE); bag[RSTONE] = 0; bagTotal = 0;');
run('{ const c = molds[0].cavs[0].slice().sort((a, b) => b - a); c.forEach((i, k) => setCell(i, k < 24 ? COPPER : SLAG, 30, k < 24 ? F_CAST : 0)); } checkMolds();');
check('a bit with slag floating on top reads as not full, not spoiled', run('molds[0].state') === 'short' && /floating/.test(run('molds[0].flaw')), run('molds[0].flaw'));
msg = run('collectMold(molds[0])');
check('the first F press on an unfinished bit only asks', run('molds[0].cavs[0].filter(i => mat[i] === COPPER).length') === 24 && /again/.test(msg), msg);
msg = run('collectMold(molds[0])');
check('a second press knocks it out as scrap', run('molds[0].cavs[0].every(i => mat[i] === EMPTY)') && run('bag[SCRCU]') === 24 && run('player.bitTier') === 0, msg);
// the igniter's flame reaches coal just under a thin layer of ore, when air gets to it from below
fresh();
run('for (let bx = 25; bx < 30; bx++) placeBlock(BP_GRATE, 0, bx, 23, true);');
rect(run, 100, 90, 119, 91, run('COAL')); rect(run, 100, 88, 119, 89, run('CUORE'));
run('for (let k = 0; k < 60; k++) { toolIgnite(110, 88); simTick(); }');
ticks(run, 120);
check('the igniter lights coal under two rows of ore over a grate', run('(()=>{for(let i=0;i<N;i++) if(flags[i]&F_BURN) return true; return false})()'));
check('and says when there is nothing to burn', run('toolIgnite(60, 60).fuel') === false);
// iron with some steel in it casts as iron; copper mixed with iron spoils the casting
fresh();
run('bag[RBRICK] = 200; bagTotal = 200; placeMoldAt("bit", 30, 20, RBRICK); bag[RBRICK] = 0; bagTotal = 0; player.bitTier = 1;');
run('{ const c = molds[0].cavs[0]; c.forEach((i, k) => setCell(i, k % 5 === 0 ? STEEL : IRON, 30, F_CAST)); } checkMolds();');
check('a bit of iron with a little steel in it is a good iron bit', run('molds[0].state') === 'ready' && run('molds[0].metal') === run('IRON'), run('molds[0].state + " " + molds[0].flaw'));
msg = run('collectMold(molds[0])');
check('and knocks out as an iron bit', run('player.bitTier') === 2, msg);
run('{ const c = molds[0].cavs[0]; c.forEach((i, k) => setCell(i, k % 5 === 0 ? COPPER : IRON, 30, F_CAST)); } checkMolds();');
check('copper mixed with iron still spoils it', run('molds[0].state') === 'flawed', run('molds[0].flaw'));
done();