From f34c34aa73e959a712df1badceab9a5930c8cf1c Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Sat, 24 Oct 2026 19:41:04 -0400 Subject: [PATCH] Bars snap to a 2-grain grid and tip into gaps, so piles stack without air (t1) Co-Authored-By: Claude Opus 5.5 --- docs/PROJECT_MAP.md | 5 +++-- src/sim/items.js | 25 +++++++++++++++++++++++-- test/t_bar_stacking.js | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 test/t_bar_stacking.js diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 02cd6fd..a486efd 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -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 - 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. -- 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 ### 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/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/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_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. diff --git a/src/sim/items.js b/src/sim/items.js index c2d8651..1bec43d 100644 --- a/src/sim/items.js +++ b/src/sim/items.js @@ -6,6 +6,12 @@ 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 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) { 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; @@ -97,22 +103,37 @@ function entTick() { const e = ents[k]; entMark(e, 0); entCurM = e.m; - const ix = e.ix, iy = e.iy; + let ix = e.ix; const iy = e.iy; const tube = entTubeAt(ix, iy); if (tube) entTubeTick(e, tube); else { // 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) { const yb = iy + BAR_H; for (let dx = 0; dx < BAR_W; dx++) { const x = ix + dx; if (x < 0 || x >= W) continue; 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; } } 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 (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 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; } diff --git a/test/t_bar_stacking.js b/test/t_bar_stacking.js new file mode 100644 index 0000000..d416e72 --- /dev/null +++ b/test/t_bar_stacking.js @@ -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();