Files
deepgrain-foundry/test/t_molds.js
T
2026-10-23 23:37:49 -04:00

199 lines
20 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);'); }
run("var moldMessages = []; const collectOriginal = collectMold; collectMold = function(mo) { const s = collectOriginal(mo); moldMessages.push(s); return s; }; 0");
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 have a warning flag', run('molds[0].blocked === true'));
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 ingot mold changes nothing', run('ents.length === 0') && cavCount(0, run('COPPER')) === 28 && /empty themselves/.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); }');
run('checkMolds()');
check('dropping bars clears the warning flag', run('molds[0].blocked === false'));
check('the next check drops bars only below it', run('ents.length === 3 && ents.every(e => e.iy === molds[0].y + molds[0].h) && !molds[0].blockedSaid'));
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 and frozen grate drips are pushed out without F
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');
check('a check pushes the plate and frozen drips out below', 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; })()`) && count(run, run('SCRCU'), 120, 92, 135, 95) === 22 && run('bagTotal === 0'));
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')) === 0 && run('bagTotal === 0'));
check('slag is thrown away without putting it in the bag or below', run('bag[SLAG] === 0 && molds[0].cavs[0].every(i => mat[i] === EMPTY)') && count(run, run('SLAG')) === 0);
// 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)}`);
// dropping bars 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();');
check('an automatic drop rakes the slag and ash off the grate', run('ents.length === 2') && count(run, run('SLAG'), 120, 72, 136, 88) + count(run, run('ASH'), 120, 72, 136, 88) === 0 && run('bag[SLAG] + bag[ASH]') === 0);
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'));
// self-cleaning waits for pours and space, and preserves loose material and temperature
function ingot() { fresh(); run('addMold("ingot", 30, 22, RSTONE, true);'); }
const under = m => count(run, run(m), 120, 92, 135, 95);
ingot();
run('setCell(molds[0].cavs[0][0], SAND, 123, 0); checkMolds();');
check('junk drops below as sand at its original temperature', under('SAND') === 1 && run('molds[0].state === "empty" && temp[92 * W + 120] === 123 && bagTotal === 0'));
ingot();
run('molds[0].cavs[0].forEach((i, k) => setCell(i, k & 1 ? COPPER : IRON, 30, F_CAST)); checkMolds();');
check('mixed metal all drops as scrap without bars', under('SCRCU') === 14 && under('SCRFE') === 14 && run('ents.length === 0 && molds[0].state === "empty" && bagTotal === 0'));
ingot();
run('setCell(molds[0].cavs[0][0], SAND, 30, 0); for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, STONE, 30, 0); checkMolds();');
check('blocked junk stays in the trough', cavCount(0, run('SAND')) === 1 && under('SAND') === 0 && run('molds[0].blocked === true'));
run('for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, EMPTY, 30, 0); checkMolds();');
check('junk drops once there is room', cavCount(0, run('SAND')) === 0 && under('SAND') === 1 && run('molds[0].blocked === false'));
ingot();
run('setCell(molds[0].cavs[0][0], SAND, 30, 0); setCell(molds[0].cavs[0][1], MCU, 1200, 0); checkMolds();');
check('cleaning waits for molten metal in the cavity', cavCount(0, run('SAND')) === 1 && under('SAND') === 0);
run('setCell(molds[0].cavs[0][1], EMPTY, 30, 0); setCell((molds[0].y - 8) * W + molds[0].x, MCU, 1200, 0); checkMolds();');
check('cleaning also waits for molten metal above the mold', cavCount(0, run('SAND')) === 1);
ingot();
run('setCell(molds[0].cavs[0][0], SAND, 30, 0); for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) part[y * W + x] = P_PIPE; checkMolds();');
check('cleaning never writes into built parts', cavCount(0, run('SAND')) === 1 && under('SAND') === 0);
// removal returns set cavity metal for every mold, separately from wall refunds
for (const kind of ['ingot', 'bit', 'tank']) {
fresh(); run(`addMold("${kind}", 30, 20, RSTONE, true); molds[0].cavs.flat().slice(0, 20).forEach(i => setCell(i, COPPER, 30, F_CAST)); removeMold(molds[0]);`);
check(`removing a ${kind} returns two bars and four scrap`, run('bag[BARCU] === 2 && bag[SCRCU] === 4') && count(run, run('COPPER')) === 0);
}
ingot();
run('molds[0].cavs[0].slice(0, 5).forEach(i => setCell(i, MCU, 1200, 0)); removeMold(molds[0]);');
check('removal leaves molten copper alone', count(run, run('MCU')) === 5 && run('bag[BARCU] === 0 && bag[SCRCU] === 0'));
fresh();
run('addMold("bit", 30, 20, RSTONE, true); molds[0].cavs[0].forEach((i, k) => setCell(i, k & 1 ? COPPER : IRON, 30, F_CAST)); removeMold(molds[0]);');
check('removing a mixed bit returns all metal as scrap', run('bag[BARCU] === 0 && bag[BARFE] === 0 && bag[SCRCU] === 15 && bag[SCRFE] === 15'));
ingot();
run('for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, STONE, 30, 0); setCell(92 * W + 120, EMPTY, 30, 0); molds[0].cavs[0].slice(0, 3).forEach(i => setCell(i, SAND, 77, 0)); checkMolds();');
check('only grains that fit are cleared', under('SAND') === 1 && cavCount(0, run('SAND')) === 2);
ingot();
run('spawnBar(BARCU, 121, 92); setCell(molds[0].cavs[0][0], SAND, 30, 0); checkMolds();');
check('loose junk never overlaps a dropped bar', under('SAND') === 1 && run('!mat.some((m, i) => m === SAND && occ[i])'));
for (const state of ['empty', 'short', 'flawed', 'ready', 'filling']) {
ingot(); run(`molds[0].state = "${state}"; setCell(molds[0].cavs[0][0], SAND, 30, 0); EV.length = 0;`);
msg = run('interactAt(molds[0].x + 1, molds[0].y + 1)');
check(`F does nothing to a ${state} ingot mold and says nothing`, msg === '' && cavCount(0, run('SAND')) === 1 && run('EV.length === 0 && bagTotal === 0 && ents.length === 0'));
}
fresh();
run('addMold("ingot", 30, 20, BARCU, true); molds[0].cavs[0].slice(0, 20).forEach(i => setCell(i, COPPER, 30, F_CAST)); removeMold(molds[0]);');
check('metal wall refund does not count cavity metal twice', run('bag[BARCU] === 2 + Math.floor(MOLDS.ingot.walls / GRAINS_PER_BAR) && bag[SCRCU] === 4'));
fresh();
run('addMold("bit", 30, 20, RSTONE, true); molds[0].cavs[0].slice(0, 20).forEach((i, k) => setCell(i, k & 1 ? IRON : STEEL, 30, F_CAST)); removeMold(molds[0]);');
check('compatible iron and steel return as iron bars and scrap', run('bag[BARFE] === 2 && bag[SCRFE] === 4 && bag[SCRST] === 0'));
for (const kind of ['bit', 'tank']) for (const metal of ['COPPER', 'IRON', 'GLASS']) for (const upgraded of [false, true]) {
fresh();
run(`addMold("${kind}", 30, 20, RSTONE, true); player.bitTier = player.tankTier = ${upgraded ? 3 : 0}; molds[0].cavs[0].forEach(i => setCell(i, ${metal}, 30, F_CAST)); checkMolds(); collectMold(molds[0]);`);
}
for (const state of ['empty', 'filling']) {
fresh(); run(`addMold("bit", 30, 20, RSTONE, true); molds[0].state = "${state}"; collectMold(molds[0]);`);
}
check('every collected mold message is short', run('moldMessages.every(s => s.length <= 90)'), run('moldMessages.join(" | ")'));
done();