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>
This commit is contained in:
2026-10-23 19:00:12 -04:00
co-authored by Claude Opus 5.5
parent 6676d99c66
commit de677b49c5
15 changed files with 140 additions and 68 deletions
+4 -1
View File
@@ -65,8 +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.
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 bars = run('ents.length'); totalBars += bars;
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) };
+5 -7
View File
@@ -254,21 +254,19 @@ with sync_playwright() as p:
check('bot can sit inside a built wall', not ev(f'botOverlapSolid({wx_ - 2}, {wy_ - 2})'))
check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})'))
# 6. ingot mold: click to place, pour, knock out bars, vacuum them
# 6. ingot mold: click to place, pour, bars drop below, vacuum them
ok = select('3', "piece().id === 'ingotmold'")
check('E cycles to the ingot mold', ok)
spot = ev(f'''(() => {{ for (let dx = -2; dx <= 10; dx++) for (let dy = 6; dy < 14; dy++) {{ const ox = {bbx} + dx, oy = {bby} - dy; if (!canPlaceMold('ingot', ox - 1, oy) && inReach(ox * 4 + 8, oy * 4 + 2)) return [ox, oy]; }} return null; }})()''')
spot = ev(f'''(() => {{ for (let dx = -2; dx <= 10; dx++) for (let dy = 6; dy < 14; dy++) {{ const ox = {bbx} + dx, oy = {bby} - dy; if (!canPlaceMold('ingot', ox - 1, oy) && [0, 1, 2].every(k => entFreeAt((ox - 1) * 4 + 1 + k * 4, oy * 4 + MOLDS.ingot.h)) && inReach(ox * 4 + 8, oy * 4 + 2)) return [ox, oy]; }} return null; }})()''')
check('found a spot for the mold', spot is not None)
mx, my = blk(spot[0], spot[1])
pg.mouse.move(mx, my); settle(100); pg.mouse.down(); pg.mouse.up(); settle(100)
check('mold placed by click', ev('molds.length') == 1, ev('GAME.status'))
ev('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)')
pg.wait_for_function("molds[0].state === 'ready'", timeout=30000)
check('bars set in the mold', ev("molds[0].state === 'ready' && molds[0].ready.length === 3"), ev('[molds[0].state, molds[0].ready.length]'))
pg.wait_for_function("ents.filter(e => e.m === BARCU).length === 3", timeout=30000)
check('bars set and drop on their own', ev("ents.length === 3 && molds[0].state === 'short' && molds[0].ready.length === 0"), ev('[molds[0].state, ents.length]'))
pg.screenshot(path=str(out / '05-mold-ready.png'))
mo = ev('[molds[0].x + 8, molds[0].y + 2]')
pg.mouse.move(*cell_to_screen(*mo)); settle(60); pg.keyboard.press('f'); settle(300)
check('F knocks out three bars', ev('ents.length') == 3, ev('GAME.status'))
check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])'))
settle(1500)
pg.screenshot(path=str(out / '06-bars.png'))
pg.keyboard.press('2'); settle(60)
+3 -4
View File
@@ -44,15 +44,14 @@ check('stone wall line placed', JSON.parse(r1).n === 5 && run('bag[RSTONE]') ===
check('wall cells are solid stone at grid positions', run('mat[80*W+100]') === run('STONE') && run('mat[83*W+119]') === run('STONE'));
const rm = run("removePieceAt(101, 81)");
check('taking a wall block back refunds its rubble', run('bag[RSTONE]') === 200 - 64, rm + ' ' + run('bag[RSTONE]'));
// 5. ingot mold: pour molten copper, knock out bars
// 5. ingot mold: pour molten copper, bars drop below
run('bag[RSTONE] = 200; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
const mo = run("placeMoldAt('ingot', 32, 22, RSTONE)");
check('ingot mold placed', mo === '' && run('molds.length') === 1, mo);
run('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)');
ticks(run, 900);
check('ingot mold sets', run('molds[0].state') === 'ready' && run('molds[0].ready.length') === 3, run('molds[0].state') + ' ' + run('molds[0].ready.length'));
const msg = run('collectMold(molds[0])');
check('knock-out spawns 3 bars', run('ents.length') === 3, msg);
check('ingot mold sets and drops three bars', run('ents.length === 3 && molds[0].state === "short"'), run('molds[0].state'));
check('bars spawn below the mold', run('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'));
ticks(run, 200);
check('bars fall and come to rest', run('ents.every(e => Math.abs(e.vy) < 0.01)'), run('JSON.stringify(ents.map(e=>[e.ix,e.iy]))'));
check('bars count as built-up copper', run('stats.bars') === 3);
+35 -9
View File
@@ -1,4 +1,4 @@
// Molds and furnaces stay usable: knock-outs leave the mold clean, plugs over the mold are
// 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');
@@ -7,13 +7,39 @@ 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
// 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;');
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]')}`);
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;');
@@ -27,9 +53,9 @@ check('the grate itself is still there', run('part[(molds[0].y - 1) * W + molds[
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'));
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('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);
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;');
+2 -2
View File
@@ -26,8 +26,8 @@ let peak = 0;
for (let t = 0; t < 6; t++) { run('for (let k = 0; k < 600; k++) simTick();'); peak = Math.max(peak, run(`(() => { let p = 0; for (let y = ${cy0}; y <= ${cy1}; y++) for (let x = ${cx0}; x <= ${cx1}; x++) p = Math.max(p, temp[y * W + x]); return Math.round(p); })()`)); }
const st = run('molds[0].state'), glassIn = run('molds[0].cavs.flat().filter(i => mat[i] === GLASS).length');
check('a fan furnace melts the sand and the molten glass runs into the ingot mold', glassIn >= 8, `peak ${peak}°C, ${sand0} sand -> ${count(run, run('SAND'))}, ${glassIn} glass in the mold, state ${st}`);
const msg = run('collectMold(molds[0])');
check('knocking it out gives glass bricks', bars(run('BARGL')) >= 2, `${bars(run('BARGL'))} bricks: ${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, AMB[y], 0); } checkMolds();');
check('opening space below drops glass bricks', bars(run('BARGL')) >= 2 && run('ents.every(e => e.iy >= molds[0].y + molds[0].h)'), `${bars(run('BARGL'))} bricks`);
check('glass bricks sell for much more than the sand in them', run('PRICE[BARGL]') >= 8 * run('PRICE[SAND]') * 10, run('PRICE[BARGL]'));
check('glass in a bit mold is too brittle to make a bit', run("METAL_TIER[GLASS]") === 0);