Files
deepgrain-foundry/test/t_molds.js
T
44r0n7andClaude Opus 5.5 de677b49c5 Ingot molds drop bars out of the bottom on their own
Bars no longer get thrown up into the furnace, and F is no longer needed to
collect them. Blocked bars wait in the trough; the short stub stays for the
next firing; F is optional upkeep (clean, chip slag, rake).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:00:12 -04:00

141 lines
15 KiB
JavaScript

// Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds 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`);
// automatic drops leave the short stub on the trough floor for the next firing
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; EV.length = 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;
check('an ingot drops three copper bars below the mold', run('ents.length === 3 && ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h && e.vy > 0 && e.T === 30)'));
check('three grains stay on the trough floor, nothing goes to the bag', cavCount(0, run('COPPER')) === 3 && run('bagTotal === 0 && molds[0].cavs[0].filter(i => mat[i] === COPPER).every(i => (i / W | 0) === molds[0].y + molds[0].h - 2)'));
check('ingots report drops instead of casting ready', run('EV.filter(e => e.t === "ingot_drop").length === 1 && !EV.some(e => e.t === "cast_ready") && stats.bars === 3 && stats.cast === 1'));
run('for (const e of ents.slice()) removeEnt(e); { const c = molds[0].cavs[0].filter(i => mat[i] === EMPTY).sort((a, b) => b - a); for (let k = 0; k < 5; k++) setCell(c[k], COPPER, 30, F_CAST); } checkMolds();');
check('five more grains top up the stub into another bar', run('ents.length === 1 && ents[0].m === BARCU && molds[0].state === "empty"') && cavCount(0, run('COPPER')) === 0);
// blocked mouths retain their metal and warn once, including molds from old saves
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; EV.length = 0; { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, STONE, 30, 0); } checkMolds(); checkMolds();');
check('blocked bars stay put and warn only once', run('ents.length === 0 && molds[0].state === "ready" && EV.filter(e => e.t === "ingot_blocked").length === 1') && cavCount(0, run('COPPER')) === 28);
msg = run('collectMold(molds[0])');
check('F on a blocked mold never launches bars upwards', run('ents.length === 0') && /open block under/.test(msg), msg);
run('{ const mo = molds[0]; for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, EMPTY, 30, 0); }');
msg = run('collectMold(molds[0])');
check('F on a ready mold drops bars only below it', run('ents.length === 3 && ents.every(e => e.iy === molds[0].y + molds[0].h) && !molds[0].blockedSaid') && /Dropped 3 bars/.test(msg), msg);
run('for (const e of ents.slice()) removeEnt(e); { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let x = mo.x; x < mo.x + mo.w; x++) setCell((mo.y + mo.h) * W + x, STONE, 30, 0); } checkMolds();');
check('a new blocked spell warns again', run('EV.filter(e => e.t === "ingot_blocked").length === 2'));
run('{ const mo = molds[0]; for (let x = mo.x; x < mo.x + mo.w; x++) setCell((mo.y + mo.h) * W + x, EMPTY, 30, 0); } checkMolds();');
check('clearing the obstruction lets the next check drop bars', run('ents.length === 3 && !molds[0].blockedSaid') && cavCount(0, run('COPPER')) === 4);
// one free mouth drops one bar; occupied mouths keep the rest for later
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); spawnBar(BARCU, mo.x + 1, mo.y + mo.h); for (let x = mo.x + 9; x < mo.x + 13; x++) setCell((mo.y + mo.h) * W + x, STONE, 30, 0); } checkMolds();');
check('partial ejection uses the free middle mouth and retains two bars', run('ents.length === 2 && ents[1].ix === molds[0].x + 5 && ents[1].iy === molds[0].y + molds[0].h && molds[0].ready.length === 2') && cavCount(0, run('COPPER')) === 20);
run('for (const e of ents.slice()) removeEnt(e); { const mo = molds[0]; for (let x = mo.x + 9; x < mo.x + 13; x++) setCell((mo.y + mo.h) * W + x, EMPTY, 30, 0); } checkMolds();');
check('remaining bars drop when mouths open', run('ents.length === 2 && molds[0].state === "short"') && cavCount(0, run('COPPER')) === 4);
// a full loose-object pool must not consume the casting or put bars in the bag
fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; for (const i of molds[0].cavs[0]) setCell(i, COPPER, 30, F_CAST); ents.length = 4000; checkMolds(); ents.length = 0;');
check('failed spawns preserve all metal and leave the bag alone', cavCount(0, run('COPPER')) === 28 && run('bagTotal === 0 && molds[0].ready.length === 3'));
// 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('slag on top does not stop automatic bars', run('ents.length === 2 && ents.every(e => e.iy >= molds[0].y + molds[0].h)') && cavCount(0, run('SLAG')) === 4 && run('bagTotal === 0'));
msg = run('collectMold(molds[0])');
check('optional F chips the slag off without putting it in the bag', run('bag[SLAG] === 0 && molds[0].cavs[0].every(i => mat[i] === EMPTY)') && /chipped off/.test(msg), 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();