Bar rack blocks: store 50 bars each, F lets them out oldest first (t1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 19:53:09 -04:00
co-authored by Claude Opus 5.5
parent f34c34aa73
commit 99d04b4972
20 changed files with 272 additions and 22 deletions
+15 -1
View File
@@ -782,8 +782,9 @@ with sync_playwright() as p:
check('and Ctrl+Z swaps the belt back', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == ev('BP_BELT') and not ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4)"), ev('GAME.status'))
# 7a. hopper blocks: the Machines tab has Grinder and Saw instead of the Refiner and Stone cutter; drag a row
check('the Machines tab has Grinder and Saw but not the Refiner or Stone cutter', ev("(() => { const ids = BUILD_CATS.find(c => c[0] === 'Machines')[1]; return ids.includes('grinder') && ids.includes('saw') && ids.includes('press') && !ids.includes('refiner') && !ids.includes('cutter'); })()"))
check('the Machines tab has Grinder, Saw, Press and Bar rack', ev("(() => { const ids = BUILD_CATS.find(c => c[0] === 'Machines')[1]; return ids.includes('grinder') && ids.includes('saw') && ids.includes('press') && ids.includes('rack') && !ids.includes('refiner') && !ids.includes('cutter'); })()"))
ev('money = 1000')
check('Bar rack has a Machines menu tile', select('3', "piece().id === 'rack'") and pg.locator('[data-piece="rack"]').count() == 1)
ok = select('3', "piece().id === 'grinder'")
ev(f"for (let y = {(bby+3)*4}; y < {(bby+4)*4}; y++) for (let x = {(bbx-8)*4}; x < {(bbx+4)*4}; x++) if (!blockP[((y/4)|0)*BW + ((x/4)|0)]) setCell(y*W+x, EMPTY, 20, 0);")
drag(lambda: blk(bbx - 6, bby + 3), lambda: blk(bbx - 4, bby + 3))
@@ -802,6 +803,19 @@ with sync_playwright() as p:
}} finally {{ removeBlock(bx, by); removeBlock(bx + 1, by); rebuildProcessors(); }}
}})()'''))
check('rack hover shows stored kinds, capacity and gate', ev(f'''(() => {{
const bx = {bbx} - 6, by = {bby} + 3;
if (placeBlock(BP_RACK, 0, bx, by, true)) return false;
try {{
const s = rackState.get(by * BW + bx); rackPush(s, BARCU, 2); rackPush(s, BARFE, 1);
const before = RS; drawRackBlocks();
const h = hoverInfo(bx * BS, by * BS);
interactAt(bx * BS, by * BS);
const opened = hoverInfo(bx * BS, by * BS);
return RS === before && h.t1 === 'Bar rack' && h.t2.includes('3 / 50 bars') && h.t2.some(t => t.includes('2 copper bar')) && h.t2.some(t => t.includes('1 iron bar')) && h.t2.includes('closed: F lets bars out') && opened.t2.includes('open: letting bars out');
}} finally {{ removeBlock(bx, by); }}
}})()'''))
# 7b. filter: choose several materials with F, place a line, replace belts with G, edit the whole line
ev('seen[CUORE] = 1; seen[FEORE] = 1; seen[SAND] = 1; money = 1000; GAME.filterCfg = { m: [], inv: 0 };')
ok = select('3', "piece().id === 'filter'")
+76
View File
@@ -0,0 +1,76 @@
// Rack intake, FIFO storage, gate, removal and saved-state validation.
const { load, suite, flatWorld } = require('./harness');
const { run } = load();
const { check, done } = suite();
function fresh(n = 1) {
flatWorld(run, 240, 160, 120);
run(`money = 5000; for (let y = 20; y < 20 + ${n}; y++) placeBlock(BP_RACK, 0, 10, y, true);`);
}
function drop(m = 'BARCU', x = 40) {
run(`spawnBar(${m}, ${x}, 72); for (let t = 0; t < 100; t++) entTick();`);
}
fresh(); drop();
check('a falling bar enters the rack', run('ents.length === 0 && rackState.get(20 * BW + 10).n === 1'));
fresh(); for (let n = 0; n < 51; n++) drop();
check('full rack leaves the 51st bar on top', run('rackState.get(20 * BW + 10).n === 50 && ents.length === 1 && ents[0].iy === 78'));
fresh(2); for (let n = 0; n < 100; n++) drop();
check('two blocks hold 100 bars', run('[...rackState.values()].every(s => s.n === 50) && !ents.length'));
check('F on the top opens the whole column', run("interactAt(40, 80) === 'Rack open.' && [...rackState.values()].every(s => s.open)"));
run('for (let t = 0; t < 20; t++) rackTick();');
check('oldest bars refill the bottom after release', run('ents.length === 1 && rackState.get(21 * BW + 10).n === 50 && rackState.get(20 * BW + 10).n === 49'));
run('for (let t = 0; t < 100; t++) rackTick();');
check('occupied outlet stops the flow', run('ents.length === 1'));
fresh(); drop('BARCU'); drop('BARFE'); drop('BARCU');
check('mixed kinds keep FIFO runs', run('JSON.stringify(rackState.get(20 * BW + 10).q) === JSON.stringify([[BARCU, 1], [BARFE, 1], [BARCU, 1]])'));
run('interactAt(40, 80);');
for (const m of ['BARCU', 'BARFE', 'BARCU']) {
run('for (let t = 0; t < 20; t++) rackTick();');
check('gate releases oldest ' + m, run(`ents.length === 1 && ents[0].m === ${m} && ents[0].iy === 84`));
run('removeEnt(ents[0]);');
}
check('empty rack stays open', run('rackState.get(20 * BW + 10).open'));
drop(); run('interactAt(40, 80); for (let t = 0; t < 40; t++) rackTick();');
check('closed gate stops output', run('ents.length === 0 && rackState.get(20 * BW + 10).n === 1'));
fresh(2); drop('BARFE'); run('rackState.get(20 * BW + 10).q = [[BARCU, 2]]; rackState.get(20 * BW + 10).n = 2;');
run('removeBlock(10, 21);');
check('removal returns contents and cash without moving upper contents', run('bag[BARFE] === 1 && money === 5040 && rackState.get(20 * BW + 10).n === 2'));
run('interactAt(40, 80); globalThis.savedRack = serializeWorld(); deserializeWorld(savedRack.meta, savedRack.buffers);');
check('save keeps queues and gate', run('rackState.get(20 * BW + 10).n === 2 && rackState.get(20 * BW + 10).open'));
run('savedRack.meta.racks = [null, {}, [-1, [[BARCU, 1]], true], [20 * BW + 10, [[BARCU, -1], [STONE, 3], [BARFE, 51], [BARCU, Infinity], null], true]]; deserializeWorld(savedRack.meta, savedRack.buffers);');
check('corrupt rack entries are ignored', run('rackState.get(20 * BW + 10).n === 0'));
fresh(); drop('BARCU', 42);
check('half of a bar over the rack is enough', run('ents.length === 0'));
fresh(); drop('BARCU', 43);
check('less than half cannot enter', run('rackState.get(20 * BW + 10).n === 0 && ents.length === 1'));
fresh(); drop();
run('interactAt(40, 80); for (let t = 0; t < 19; t++) rackTick();');
check('gate waits 20 ticks for each bar', run('ents.length === 0'));
run('setCell(84 * W + 40, STONE, 20, 0); for (let t = 0; t < 40; t++) rackTick();');
check('solid ground under the outlet also blocks it', run('ents.length === 0 && rackState.get(20 * BW + 10).n === 1'));
run('setCell(84 * W + 40, EMPTY, 20, 0); rackTick();');
check('clearing the outlet resumes output', run('ents.length === 1'));
fresh(); drop('BARFE');
run('bag.fill(0); bag[SAND] = bagCap(); bagTotal = bagCap(); removeBlock(10, 20);');
check('a full bag drops stored bars near the bot', run('bag[BARFE] + ents.filter(e => e.m === BARFE).length === 1 && !rackState.size'));
fresh();
run('interactAt(40, 80); placeBlock(BP_RACK, 0, 10, 21, true);');
check('extending an open rack keeps its gate open', run('[...rackState.values()].every(s => s.open)'));
fresh();
run('removeBlock(10, 20); undoRecord(\'rack\', () => placeBlock(BP_RACK, 0, 10, 20, false)); undoLast();');
check('rack building can be undone and refunded', run('!blockP[20 * BW + 10] && !rackState.size && money === 5040'));
fresh();
run('globalThis.oldRack = serializeWorld(); delete oldRack.meta.racks; deserializeWorld(oldRack.meta, oldRack.buffers);');
check('missing rack metadata defaults to empty and closed', run('rackState.get(20 * BW + 10).n === 0 && !rackState.get(20 * BW + 10).open'));
fresh(3);
run('rackPush(rackState.get(20 * BW + 10), BARFE, 3); rackPush(rackState.get(22 * BW + 10), BARCU, 1); interactAt(40, 84); for (let t = 0; t < 20; t++) rackTick();');
check('refilling crosses an empty middle block without losing FIFO order', run('ents[0].m === BARCU && rackState.get(22 * BW + 10).n === 3 && rackState.get(20 * BW + 10).n === 0'));
for (const delivery of ['belt', 'tube', 'thrown', 'mold']) {
fresh();
if (delivery === 'belt') run('for (let bx = 6; bx <= 9; bx++) placeBlock(BP_BELT, 1, bx, 20, true); spawnBar(BARCU, 30, 78);');
if (delivery === 'tube') run('placeBlock(BP_TUBE, 2, 10, 17, true); placeBlock(BP_TUBE, 2, 10, 18, true); spawnBar(BARCU, 40, 68);');
if (delivery === 'thrown') run('spawnBar(BARCU, 34, 66, 0.35, -0.3);');
if (delivery === 'mold') run("addMold('ingot', 10, 17, RSTONE, true); for (const i of molds[0].cavs[0].slice(0, 8)) setCell(i, COPPER, 20, F_CAST); checkMolds();");
run('for (let t = 0; t < 300; t++) simTick();');
check(delivery + ' delivery reaches storage', run('rackState.get(20 * BW + 10).n > 0'), run('JSON.stringify(ents.map(e => [e.m,e.ix,e.iy,e.vx,e.vy]))'));
}
done();