Ingot molds empty themselves (no F); removing a mold returns its set metal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 22:53:38 -04:00
co-authored by Claude Opus 5.5
parent 9addc43be1
commit 43eb4e23fd
13 changed files with 146 additions and 43 deletions
+2 -2
View File
@@ -65,11 +65,11 @@ for (let f = 0; f < firings; f++) {
} else ticks(run, 5400);
const st = run(`JSON.stringify(molds.map(m => m.state + (m.flaw ? ':' + m.flaw : '')))`);
if (process.env.DUMP === 'each') { console.log('-- before knock-out, firing', f + 1, st); dump(); }
// Vacuum earlier drops, open the outlet, then do the optional F upkeep.
// Vacuum earlier drops, open the outlet, let the ingot molds empty themselves.
let bars = run('ents.length');
run('for (const e of ents.slice()) removeEnt(e); for (const mo of molds) if (mo.kind === "ingot") 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, AMB[y], 0); checkMolds();');
bars += run('ents.length'); totalBars += bars;
const msgs = JSON.parse(run(`JSON.stringify(molds.map(m => collectMold(m)))`));
const msgs = JSON.parse(run(`JSON.stringify(molds.map(m => m.kind === "ingot" ? "Ingot molds empty themselves." : collectMold(m)))`));
if (run('player.bitTier') > bits) bits = run('player.bitTier');
run('for (const e of ents.slice()) removeEnt(e);');
const left = { ore: count(run('CUORE'), false), coal: count(run('COAL'), false), slag: count(run('SLAG'), false), ash: count(run('ASH'), false), copperOut: count(run('COPPER'), false) + count(run('SCRCU'), false) };
+59 -12
View File
@@ -22,10 +22,10 @@ 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);
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); }');
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('checkMolds()');
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();');
@@ -40,22 +40,20 @@ check('remaining bars drop when mouths open', run('ents.length === 2 && molds[0]
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
// 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');
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('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')) === 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);
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;');
@@ -103,13 +101,12 @@ check('ore does not sift down through unlit coal', Math.abs(y1 - y0) < 0.3, `${y
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
// 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();');
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('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();
@@ -137,4 +134,54 @@ 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('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);
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 makes no sound`, /empty themselves/.test(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'));
done();