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 f540c5454f
commit 3e313e20eb
7 changed files with 80 additions and 11 deletions
+3 -2
View File
@@ -7,8 +7,6 @@ and stay there until the owner has checked them in the game and deleted them.
## To do
- stacking ingots is a little odd, they dont stack neatly. so putting them all in the same place means it will get full but have lots of air space. what can we do about this? WHat in real life might be the solution for storing ingots?
- i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt. update: i found that its because when there is a screw tube at the end of the belt, it stops the lower grains and only the ones at the top move. the screw tube shouldnt block them, it should suck them in.
- (agent) Written after version 24, which already tried to fix this: carryColumn in src/sim/conveyors.js moves the whole stack, up to 96 grains deep. A one-off headless check of a tall heap moving together passed then (it was not kept as a suite test), so the real game must differ from that check somehow. Reproduce it in the game first (tools/shot.py, or play it), then add a suite test that fails, then fix.
- instead of full description on to left for build menu, mouse hover reviels a new info popup. organize the popup look so its nice and easy to read the information.
- dont need the short description in the build menu if we have a hover info popup.
- lets think about how best to design the factory products. basics are normal smelting ore, refining, etc. but the actual export products could be like real construction materials. materials that a real construction site might need.
@@ -27,6 +25,9 @@ and stay there until the owner has checked them in the game and deleted them.
## Done, please check
- i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt. update: i found that its because when there is a screw tube at the end of the belt, it stops the lower grains and only the ones at the top move. the screw tube shouldnt block them, it should suck them in.
- (agent) Written after version 24, which already tried to fix this: carryColumn in src/sim/conveyors.js moves the whole stack, up to 96 grains deep. A one-off headless check of a tall heap moving together passed then (it was not kept as a suite test), so the real game must differ from that check somehow. Reproduce it in the game first (tools/shot.py, or play it), then add a suite test that fails, then fix.
- (agent) Found it: when the front of the line backs up (your tube taking less than the belt brings, or a wall), the bottom grains were blocked but the ones above them still had air ahead, so the top slid forward over a stuck bottom forever. Now a column on a belt moves as a unit: if a grain can't move, nothing above it moves either, so a backed-up heap stands still (the belt slips under it) and is fed into the tube from its front, bottom layers included. A free-running belt still carries the whole heap. To check: blow a heap of two materials onto a belt that ends in an up tube and watch the bottom material go into the tube too. tools/scenes/belt-heap.js sets up the same thing for screenshots.
- copper bars go into the furnace. also they shouldnt require the player to press F to collect. instead it should automatically eject ingots below and the player can have a belt there.
- (agent) Bars now drop out of the bottom of an ingot mold by themselves as soon as they set, never up into the furnace. Leave one open block under the mold and put a belt below that: the bars land on it and ride off (a belt touching the mold's underside counts as blocked). If there is no room, the bars wait in the mold and a message says so once. The bit of metal too short for a bar stays in the trough for the next firing. F on the mold is now optional upkeep: it empties the trough, chips off slag and rakes the furnace grate. To check: run a furnace over a mold with a belt a block below.
+3 -2
View File
@@ -140,8 +140,9 @@ above, including:
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,
build more lines.
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. The owner
reports this still misbehaves (see NOTES.md).
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. If a grain is blocked,
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,
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,
+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).
- functions: engineIntake, fanBlow, machinesTick
### src/sim/conveyors.js (109 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.
### 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). 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
- 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.
- 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.
- 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)
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/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/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/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
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)
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.
// 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.
// 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.
const CARRY_DEPTH = 96;
function carryColumn(a, d, pass) {
let blocked = false;
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
if (a < 0) return;
const m = mat[a];
@@ -16,12 +17,16 @@ function carryColumn(a, d, pass) {
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 (blocked) continue; // filters still sift above a stopped load
const x = a % W, nx = x + d;
if (nx < 0 || nx >= W) return;
const j = a + d, tj = mat[j];
// 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);
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;
// 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);
+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('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');
// --- 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();
+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];