Belts: a backed-up heap stands still instead of the top sliding over the bottom

carryColumn stops at the first blocked grain on a belt, so a heap whose front
is backed up (a screw tube taking less than the belt brings, or a wall) no
longer shears. New t_conveyors checks and a belt-heap screenshot scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 19:07:28 -04:00
co-authored by Claude Opus 5.5
parent de677b49c5
commit caa2463fda
6 changed files with 77 additions and 9 deletions
+3 -2
View File
@@ -140,8 +140,9 @@ above, including:
short stubs stay for the next firing. F stays available for cleaning and raking. short stubs stay for the next firing. F stays available for cleaning and raking.
- **Screw tubes carry no more than belts:** they move every other tick. For more throughput, - **Screw tubes carry no more than belts:** they move every other tick. For more throughput,
build more lines. build more lines.
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. The owner - **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. If a grain is blocked,
reports this still misbehaves (see NOTES.md). the load above it stands still and the belt slips underneath; the top no longer slides over
a stuck bottom. Filters still let their chosen materials drop through.
- **Pick** (middle mouse) selects the piece under the cursor with its direction, - **Pick** (middle mouse) selects the piece under the cursor with its direction,
wall material and filter settings. G is unbound, as the owner requested. wall material and filter settings. G is unbound, as the owner requested.
- **The build menu** is a bar of tabs and icon tiles along the bottom: Structure, Moving, - **The build menu** is a bar of tabs and icon tiles along the bottom: Structure, Moving,
+5 -4
View File
@@ -248,8 +248,8 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick). Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
- functions: engineIntake, fanBlow, machinesTick - functions: engineIntake, fanBlow, machinesTick
### src/sim/conveyors.js (109 lines) ### src/sim/conveyors.js (114 lines)
How belts, filters and screw tubes move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear), the way a real belt carries a heap, not just its bottom layer. A filter's own material drops through it instead, unless the filter below is already full. How belts, filters and screw tubes move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeIntake - functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeIntake
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty - variables: CARRY_DEPTH, filterR, filterL, filtersDirty
@@ -257,10 +257,10 @@ How belts, filters and screw tubes move what is on or in them. What rests on a b
Building blocks: placing, integrity, removing, lines and rectangles of blocks. Building blocks: placing, integrity, removing, lines and rectangles of blocks.
- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks - functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
### src/sim/molds.js (297 lines) ### src/sim/molds.js (298 lines)
Molds: cavities for castings; ingot troughs drop bars out of the bottom. Molds: cavities for castings; ingot troughs drop bars out of the bottom.
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, knockNote, tally, collectMold, removeMold - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, knockNote, tally, collectMold, removeMold
- variables: DROSS, grains - variables: DROSS, grains, barWord
### src/sim/items.js (134 lines) ### src/sim/items.js (134 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.
@@ -467,5 +467,6 @@ The frame loop and boot.
- `tools/experiments/smelter.js`: Smelter design experiments, built with the same block pieces a player uses. Layout (block rows, 4x4 grains each), on flat ground at row 25: roof with a vent ........ row g-CH-1 chamber, CH blocks tall .. charge of coal + copper ore inside grate floor ............. row g = 23 (liquids pass, powders do not) ingot mold .............. row 24, sitting on the ground side walls run from the roof down past the mold. Usage: node t_smelter.js [key=value ...] - `tools/experiments/smelter.js`: Smelter design experiments, built with the same block pieces a player uses. Layout (block rows, 4x4 grains each), on flat ground at row 25: roof with a vent ........ row g-CH-1 chamber, CH blocks tall .. charge of coal + copper ore inside grate floor ............. row g = 23 (liquids pass, powders do not) ingot mold .............. row 24, sitting on the ground side walls run from the roof down past the mold. Usage: node t_smelter.js [key=value ...]
- `tools/experiments/starter.js`: Plays a starter design the way a player would: paste the blueprint, let the bot build it from the bag, blow an even ore/coal mix inside, light it, wait, knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal] - `tools/experiments/starter.js`: Plays a starter design the way a player would: paste the blueprint, let the bot build it from the bag, blow an even ore/coal mix inside, light it, wait, knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
- `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost. - `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost.
- `tools/scenes/belt-heap.js`: A belt feeding an upward screw: sand underneath a deep heap of copper ore. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
- `tools/scenes/clear-sky.js`: A clear work area left of the bot's starting point (the trade dock is to its right): open sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block of the area that stands on the floor) and CAM (the middle of the area). - `tools/scenes/clear-sky.js`: A clear work area left of the bot's starting point (the trade dock is to its right): open sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block of the area that stands on the floor) and CAM (the middle of the area).
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7 - `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
+20
View File
@@ -4,6 +4,26 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-23: a backed-up heap on a belt no longer shears (Claude Code, with Codex)
Note: "belts with a big pile on top only move the grains on the top of the stack…". The owner's
setup: a belt ending in a screw tube going up; material mixed into the bottom layers never moved.
- Cause: `carryColumn` (`src/sim/conveyors.js`) skipped a blocked grain and carried on up the
column, so with the front backed up the top slid over a stuck bottom. Reproduced headlessly
first: over 4,000 ticks, 8,815 shear steps at the tube (1,415 against a wall) and only 57 of
192 bottom sand grains left the belt.
- Fix: on a belt, the walk stops at the first blocked powder grain, so the load above stands
still. On a filter the walk goes on only so chosen materials can still drop through; nothing
above a blocked grain is carried. After: no shear, 119 of 192 sand grains out through the tube.
- `test/t_conveyors.js`: 3 new checks (no shear into an up tube, none against a wall, bottom
layer leaves through the tube). `tools/scenes/belt-heap.js` sets up the owner's case for
`tools/shot.py`; shots at 30 and 900 ticks look right. `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Tests: all 11 headless suites and `play.py` (109) pass. `t_smooth.py` failed its
half-resolution check once at load 6.5 and passed on the rerun.
- Open: `t_smooth.py`'s half-resolution check is flaky on a busy machine (the game drops to
pixel when frames are slow). It is worth making that test immune to the slow fallback.
## 2026-10-23: ingots drop out of the bottom of the mold (Claude Code, with Codex) ## 2026-10-23: ingots drop out of the bottom of the mold (Claude Code, with Codex)
Note: "copper bars go into the furnace. also they shouldnt require the player to press F…". Note: "copper bars go into the furnace. also they shouldnt require the player to press F…".
+8 -3
View File
@@ -1,10 +1,11 @@
// How belts, filters and screw tubes move what is on or in them. // How belts, filters and screw tubes move what is on or in them.
// What rests on a belt or filter rides along: the whole stack standing on the carrying surface // What rests on a belt or filter rides along: the whole stack standing on the carrying surface
// steps one cell toward d together (belts run front of the line first, so the way ahead is // steps one cell toward d together (belts run front of the line first, so the way ahead is
// clear), the way a real belt carries a heap, not just its bottom layer. A filter's own // clear). If a powder grain is blocked, the load above it stands still and the belt slips
// material drops through it instead, unless the filter below is already full. // underneath. A filter's own material drops through it instead, unless it is already full.
const CARRY_DEPTH = 96; const CARRY_DEPTH = 96;
function carryColumn(a, d, pass) { function carryColumn(a, d, pass) {
let blocked = false;
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) { for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
if (a < 0) return; if (a < 0) return;
const m = mat[a]; const m = mat[a];
@@ -16,12 +17,16 @@ function carryColumn(a, d, pass) {
const u = mat[a + W]; const u = mat[a + W];
if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); continue; } if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); continue; }
} }
if (blocked) continue; // filters still sift above a stopped load
const x = a % W, nx = x + d; const x = a % W, nx = x + d;
if (nx < 0 || nx >= W) return; if (nx < 0 || nx >= W) return;
const j = a + d, tj = mat[j]; const j = a + d, tj = mat[j];
// grains on a wet belt push through the water rather than stopping at it // grains on a wet belt push through the water rather than stopping at it
const wet = PH[tj] === PH_LIQUID && !occ[j] && stamp[j] !== tickStamp && (!part[j] || part[j] === P_FILTER || part[j] === P_GOPEN); const wet = PH[tj] === PH_LIQUID && !occ[j] && stamp[j] !== tickStamp && (!part[j] || part[j] === P_FILTER || part[j] === P_GOPEN);
if (!wet && !enter(m, PH_POWDER, j, d, 0)) continue; if (!wet && !enter(m, PH_POWDER, j, d, 0)) {
if (!pass) return;
blocked = true; continue;
}
const y = (a / W) | 0, bj = j + W; const y = (a / W) | 0, bj = j + W;
// carried off the end with nothing under it: it leaves at belt speed and falls // carried off the end with nothing under it: it leaves at belt speed and falls
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y, d * 0.5, 0); if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y, d * 0.5, 0);
+32
View File
@@ -157,4 +157,36 @@ r = JSON.parse(run('JSON.stringify(placeBlocks(BP_WALL, RSTONE, blockLine(20, 25
check('and a wall can replace them too', r.n === 2 && run('blockP[25*BW+20]') === run('BP_WALL') && run('!filterCfg.has(25*BW+20)'), JSON.stringify(r)); check('and a wall can replace them too', r.n === 2 && run('blockP[25*BW+20]') === run('BP_WALL') && run('!filterCfg.has(25*BW+20)'), JSON.stringify(r));
check('filters are a buildable piece with a price', run('PIECE_BY_ID.filter && !PIECE_BY_ID.filter.hidden && PIECE_BY_ID.filter.cost[0][0] === CASH')); check('filters are a buildable piece with a price', run('PIECE_BY_ID.filter && !PIECE_BY_ID.filter.hidden && PIECE_BY_ID.filter.cost[0][0] === CASH'));
check('no trucks', run('typeof truck') === 'undefined'); check('no trucks', run('typeof truck') === 'undefined');
// --- a backed-up heap must not shear over its bottom layer
// Watch belt beats, not settling between beats: copper must not ride over sand that stays put.
run(`globalThis.__carry = carryColumn;
globalThis.__shear = 0;
carryColumn = function(a, d, pass) {
const bottom = a, watch = [];
const sand = mat[a] === SAND && stamp[a] !== tickStamp && !occ[a];
if (sand) for (let i = a - W; i >= 0 && mat[i] !== EMPTY; i -= W) {
if (mat[i] === CUORE && stamp[i] !== tickStamp) watch.push(i);
}
__carry(a, d, pass);
if (sand && mat[bottom] === SAND && stamp[bottom] !== tickStamp)
for (const i of watch) if (mat[i] !== CUORE) __shear++;
};`);
for (const lift of [true, false]) {
fresh();
run('RS = 7; __shear = 0; for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
if (lift) run('for (let by = 15; by <= 25; by++) placeBlock(BP_TUBE, 0, 32, by, true);');
else run('for (let by = 10; by <= 25; by++) placeBlock(BP_WALL, RSTONE, 32, by, true);');
rect(run, 88, 94, 119, 99, run('SAND'));
rect(run, 88, 70, 119, 93, run('CUORE'));
ticks(run, 4000);
const shear = run('__shear');
check(lift ? 'a heap feeding an upward screw does not shear over stuck sand' : 'a heap against a wall does not shear over stuck sand', shear === 0, `${shear} copper steps over stationary sand in 4000 ticks`);
if (lift) {
const left = count(run, run('SAND'), 80, 0, 127, 99);
const lifted = count(run, run('SAND'), 128, 0, 319, 93);
check('the bottom sand leaves the belt through the upward screw too', left < 192 / 2 && lifted > 0, `${192 - left} of 192 sand left the belt, ${lifted} above its bottom layer at the lift`);
}
}
run('carryColumn = __carry;');
done(); done();
+9
View File
@@ -0,0 +1,9 @@
// A belt feeding an upward screw: sand underneath a deep heap of copper ore.
// Run clear-sky.js first, e.g.
// python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
const bx = BX + 6, by = BY;
for (let x = bx; x < bx + 12; x++) placeBlock(BP_BELT, 1, x, by, true);
for (let y = by - 10; y <= by; y++) placeBlock(BP_TUBE, 0, bx + 12, y, true);
for (let y = by * 4 - 30; y < by * 4; y++) for (let x = (bx + 2) * 4; x < (bx + 10) * 4; x++)
setCell(y * W + x, y >= by * 4 - 6 ? SAND : CUORE, AMB[y], 0);
window.CAM = [(bx + 7) * 4, (by - 5) * 4];