Bars snap to a 2-grain grid and tip into gaps, so piles stack without air (t1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 19:41:04 -04:00
co-authored by Claude Opus 5.5
parent 68c021f64b
commit 232f5af6e4
3 changed files with 67 additions and 4 deletions
+3 -2
View File
@@ -269,9 +269,9 @@ Molds: cavities for castings; ingot troughs empty themselves below. Removal retu
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord - variables: DROSS, grains, barWord
### src/sim/items.js (203 lines) ### src/sim/items.js (224 lines)
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds. Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
- functions: entSolidCell, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeEnter, entTubeTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText - functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeEnter, entTubeTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
- variables: BAR_W, BAR_H, entCurM, droppedLast - variables: BAR_W, BAR_H, entCurM, droppedLast
### src/sim/tools.js (52 lines) ### src/sim/tools.js (52 lines)
@@ -458,6 +458,7 @@ The frame loop and boot.
- `test/harness.js`: Loads the simulation (the src/sim/ files listed in src/manifest.json, joined in order into one script, the same way build.py joins them) so scenarios can drive it headlessly in Node. Errors point at the real file and line (sim/pipes.js:42), not at the joined script. - `test/harness.js`: Loads the simulation (the src/sim/ files listed in src/manifest.json, joined in order into one script, the same way build.py joins them) so scenarios can drive it headlessly in Node. Errors point at the real file and line (sim/pipes.js:42), not at the joined script.
- `test/play.py`: Play-test in a real browser: mouse, keyboard and a mocked gamepad drive the game; save and continue. Screenshots go to shots/play/ (or the folder given as the first argument). Pixel look unless the second argument is 'smooth'. - `test/play.py`: Play-test in a real browser: mouse, keyboard and a mocked gamepad drive the game; save and continue. Screenshots go to shots/play/ (or the folder given as the first argument). Pixel look unless the second argument is 'smooth'.
- `test/run_all.py`: Run the test suites and report. - `test/run_all.py`: Run the test suites and report.
- `test/t_bar_stacking.js`: Resting bars snap into pairs of grains and tip off narrow supports.
- `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load. - `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
- `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning. - `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
- `test/t_conveyors.js`: Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters. - `test/t_conveyors.js`: Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
+23 -2
View File
@@ -6,6 +6,12 @@
const BAR_W = 4, BAR_H = 2; const BAR_W = 4, BAR_H = 2;
let entCurM = 0; // the bar being moved, so a filter can let its own kind of bar through let entCurM = 0; // the bar being moved, so a filter can let its own kind of bar through
function entSolidCell(j) { const m = mat[j], p = part[j]; return occ[j] || PH[m] === PH_SOLID || PH[m] === PH_POWDER || (p !== 0 && (p <= P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); } function entSolidCell(j) { const m = mat[j], p = part[j]; return occ[j] || PH[m] === PH_SOLID || PH[m] === PH_POWDER || (p !== 0 && (p <= P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); }
function entSupportCount(x, y) {
if (y + BAR_H >= H) return BAR_W;
let n = 0;
for (let dx = 0; dx < BAR_W; dx++) if (entSolidCell((y + BAR_H) * W + x + dx)) n++;
return n;
}
function entFreeAt(x, y) { function entFreeAt(x, y) {
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false; if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false;
for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) if (entSolidCell((y + dy) * W + x + dx)) return false; for (let dy = 0; dy < BAR_H; dy++) for (let dx = 0; dx < BAR_W; dx++) if (entSolidCell((y + dy) * W + x + dx)) return false;
@@ -97,22 +103,37 @@ function entTick() {
const e = ents[k]; const e = ents[k];
entMark(e, 0); entMark(e, 0);
entCurM = e.m; entCurM = e.m;
const ix = e.ix, iy = e.iy; let ix = e.ix; const iy = e.iy;
const tube = entTubeAt(ix, iy); const tube = entTubeAt(ix, iy);
if (tube) entTubeTick(e, tube); if (tube) entTubeTick(e, tube);
else { else {
// what is underneath? // what is underneath?
let belt = 0, bsat = 0, support = iy + BAR_H >= H; let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
if (!support) { if (!support) {
const yb = iy + BAR_H; const yb = iy + BAR_H;
for (let dx = 0; dx < BAR_W; dx++) { for (let dx = 0; dx < BAR_W; dx++) {
const x = ix + dx; if (x < 0 || x >= W) continue; const x = ix + dx; if (x < 0 || x >= W) continue;
const j = yb * W + x, m = mat[j]; const j = yb * W + x, m = mat[j];
if (m === BELTL || m === BELTR || part[j] === P_FILTER) transport = true;
if (m === BELTL || m === BELTR) { const s = netSat(j); if (s > bsat) { bsat = s; belt = m === BELTR ? 1 : -1; } } if (m === BELTL || m === BELTR) { const s = netSat(j); if (s > bsat) { bsat = s; belt = m === BELTR ? 1 : -1; } }
else if (part[j] === P_FILTER && !filterPassAt(e.m, j) && (yb & 3) === 0) { bsat = 1; belt = blockV[(yb >> 2) * BW + (x >> 2)] & 128 ? 1 : -1; } else if (part[j] === P_FILTER && !filterPassAt(e.m, j) && (yb & 3) === 0) { bsat = 1; belt = blockV[(yb >> 2) * BW + (x >> 2)] & 128 ? 1 : -1; }
if (entSolidCell(j)) support = true; if (entSolidCell(j)) support = true;
} }
} }
if (support && !transport && e.vy === 0 && Math.abs(e.vx) < 0.05) {
const n = entSupportCount(ix, iy);
if (n < 2) {
let left = 0, right = 0;
for (let dx = 0; dx < BAR_W; dx++) if (!entSolidCell((iy + BAR_H) * W + ix + dx)) {
if (dx < BAR_W / 2) left++; else right++;
}
const dir = left > right ? -1 : right > left ? 1 : ((ix + iy) & 1) ? 1 : -1;
if (entFreeAt(ix + dir, iy)) ix += dir;
} else if (ix & 1) {
for (let dir = -1; dir <= 1; dir += 2) if (entFreeAt(ix + dir, iy) && entSupportCount(ix + dir, iy) >= 2) { ix += dir; break; }
}
if (ix !== e.ix) { e.x = e.ix = ix; support = entSupportCount(ix, iy) > 0; }
}
const ci = Math.min(H - 1, iy + 1) * W + Math.min(W - 1, ix + 1); const ci = Math.min(H - 1, iy + 1) * W + Math.min(W - 1, ix + 1);
const inLiq = PH[mat[ci]] === PH_LIQUID; const inLiq = PH[mat[ci]] === PH_LIQUID;
if (support) { if (e.vy > 0) e.vy = 0; e.vx = belt ? belt * 0.5 * bsat : e.vx * 0.55; } if (support) { if (e.vy > 0) e.vy = 0; e.vx = belt ? belt * 0.5 * bsat : e.vx * 0.55; }
+41
View File
@@ -0,0 +1,41 @@
// Resting bars snap into pairs of grains and tip off narrow supports.
const { load, suite, flatWorld, rect } = require('./harness');
const { run } = load();
const { check, done } = suite();
function fresh() { flatWorld(run, 240, 160, 100); }
function settle() { run('for (let n = 0; n < 240; n++) entTick();'); }
fresh();
rect(run, 39, 60, 39, 99, run('STONE'));
rect(run, 56, 60, 56, 99, run('STONE'));
for (const x of [49, 41, 52, 45, 45, 52, 41, 49, 52, 49, 45, 41]) {
check('a dropped bar fits above the pit', !!run(`spawnBar(BARCU, ${x}, 64)`)); settle();
}
check('twelve bars fill the bottom six grain rows without gaps', run(`(() => {
for (let y = 94; y < 100; y++) for (let x = 40; x < 56; x++) if (!occ[y * W + x]) return false;
return ents.length === 12;
})()`), run('JSON.stringify(ents.map(e => [e.ix, e.iy]))'));
for (const side of [-1, 1]) {
fresh();
rect(run, side < 0 ? 43 : 40, 80, side < 0 ? 43 : 40, 99, run('STONE'));
run('spawnBar(BARCU, 40, 78)'); settle();
check('one-cell support tips toward the empty side ' + side, run(`ents[0].iy === 98 && ents[0].ix ${side < 0 ? '<' : '>'} 40`));
}
fresh(); rect(run, 40, 80, 41, 99, run('STONE'));
run('spawnBar(BARCU, 40, 78)'); settle();
check('half-supported bar stays on its ledge', run('ents[0].ix === 40 && ents[0].iy === 78'));
fresh(); run('for (const x of [41, 51, 61, 71]) spawnBar(BARCU, x, 70)'); settle();
check('bars settle on even columns on flat ground', run('ents.every(e => !(e.ix & 1) && e.iy === 98)'));
for (const obstacle of ['SAND', 'bar']) {
fresh(); rect(run, 41, 80, 44, 99, run('STONE'));
if (obstacle === 'bar') run('spawnBar(BARFE, 37, 78); spawnBar(BARFE, 45, 78)');
else { rect(run, 40, 78, 40, 79, run('SAND')); rect(run, 45, 78, 45, 79, run('SAND')); }
run('spawnBar(BARCU, 41, 78); entTick()');
check('snap cannot enter ' + obstacle, run('ents.find(e => e.m === BARCU).ix === 41'));
}
for (const surface of ['BELTR', 'P_FILTER']) {
fresh();
run(`for (let x = 40; x < 48; x++) ${surface === 'BELTR' ? 'setCell(80 * W + x, BELTR, 20, 0)' : 'part[80 * W + x] = P_FILTER'}; spawnBar(BARCU, 41, 78)`);
run('entTick()');
check('settling leaves transport in control on ' + surface, run(surface === 'BELTR' ? 'ents[0].ix === 42' : 'ents[0].ix === 41'));
}
done();