Screw tubes use all their lanes: grains spread sideways inside a tube

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 00:00:05 -04:00
co-authored by Claude Opus 5.5
parent 971bcc2558
commit 92e2d1c19d
6 changed files with 122 additions and 13 deletions
+6
View File
@@ -188,6 +188,12 @@ above, including:
- **The silo has pixel art in the pixel look**, drawn grain by grain like the rest of the world; - **The silo has pixel art in the pixel look**, drawn grain by grain like the rest of the world;
the smooth look keeps the illustrated silo. The owner noticed it looked the same in both. the smooth look keeps the illustrated silo. The owner noticed it looked the same in both.
- **Screw tubes use all their lanes.** Grains in an up or down tube shift sideways toward the
emptier lanes of the block, and grains taken in from the side go to the far lanes first. A
block-wide lift fed by a belt from the side now carries about 90% of what the belt brings
(before, only the edge lane filled: the owner saw one line on either side). Belts keep their
speed.
## Ideas not built yet ## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble. - **Concrete**, as a stronger wall material from rubble.
+3 -3
View File
@@ -244,13 +244,13 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick - functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty - variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
### src/sim/machine-tick.js (158 lines) ### src/sim/machine-tick.js (166 lines)
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 (114 lines) ### src/sim/conveyors.js (127 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. 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, tubeSpread, tubeIntake
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty - variables: CARRY_DEPTH, filterR, filterL, filtersDirty
### src/sim/blocks.js (132 lines) ### src/sim/blocks.js (132 lines)
+21
View File
@@ -4,6 +4,27 @@ 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: screw tubes use all their lanes (Claude Code, with Codex)
Note: "maybe the belts can move faster/carry more than the tubes… the tubes only carry one line of
materials on either side." The owner chose to fill every lane; belt speed is unchanged.
- Measured first (`test/t_conveyors.js`, a belt with a sand heap into the side of a 6-block up
tube, 3,000 ticks): 1,459 grains out, lanes `[2940, 0, 0, 10]`; the edge lane was full (one
lane carries at most ~1,500). Spreading only at intake changed little (1,486): belts push
straight into the edge lane without going through `tubeIntake`.
- `src/sim/machine-tick.js`: on each tube beat a grain in a vertical tube may step sideways
within its block to a lane with clearly fewer grains ahead (a strict gap stops shuffling).
`src/sim/conveyors.js`: `tubeSpread`, shared by side intake (far lanes first) and corners.
- After: 2,071 out (92% of the 2,243 the same belt delivers to an open drop), lanes
`[1937, 1116, 486, 872]`, sand conserved. Checks: all four lanes, conservation, at least 90%
of the belt's delivery, a side-fed sideways tube fills more than one row. All 11 headless
suites and `play.py` pass. `docs/DESIGN.md`, `docs/PROJECT_MAP.md`.
- Found on the way: Codex's sandbox (`workspace-write`) blocks network sockets, and headless
Chrome crashes at startup without one (SIGTRAP core dumps in pairs). Codex runs now pass
`-c sandbox_workspace_write.network_access=true`.
- Open: nothing.
## 2026-10-23: the build card describes the build tool, not the piece (Claude Code) ## 2026-10-23: the build card describes the build tool, not the piece (Claude Code)
Note: "instead of the current info box for the build tool showing the current building selected, Note: "instead of the current info box for the build tool showing the current building selected,
+13
View File
@@ -91,6 +91,18 @@ function filterRun(b) {
// A cell a grain can be pushed into: empty, or liquid or gas that simply trades places with it // A cell a grain can be pushed into: empty, or liquid or gas that simply trades places with it
function roomFor(j) { const t = mat[j]; return t === EMPTY || PH[t] === PH_GAS || PH[t] === PH_LIQUID; } function roomFor(j) { const t = mat[j]; return t === EMPTY || PH[t] === PH_GAS || PH[t] === PH_LIQUID; }
// Cross one tube block, far lanes first; corners retain their liquid/gas swaps.
function tubeSpread(c, x, y, sx, sy, intake = false) {
const p = part[c], bx = x >> 2, by = y >> 2;
for (;;) {
const nx = x + sx, ny = y + sy;
if (nx < 0 || ny < 0 || nx >= W || ny >= H || (nx >> 2) !== bx || (ny >> 2) !== by) break;
const j = ny * W + nx;
if (part[j] !== p || occ[j] || (intake ? mat[j] !== EMPTY || stamp[j] === tickStamp : !roomFor(j))) break;
move(c, j, x, y, nx, ny); c = j; x = nx; y = ny;
}
return c;
}
// An empty screw cell draws in loose material from behind it or from its sides // An empty screw cell draws in loose material from behind it or from its sides
// (not from the cell it pushes into), so a tube end sitting in a pile feeds itself. // (not from the cell it pushes into), so a tube end sitting in a pile feeds itself.
function tubeIntake(i, p, dx, dy) { function tubeIntake(i, p, dx, dy) {
@@ -109,6 +121,7 @@ function tubeIntake(i, p, dx, dy) {
if (pj >= P_TU && pj <= P_TR) continue; // other tube cells move their own contents if (pj >= P_TU && pj <= P_TR) continue; // other tube cells move their own contents
if (pj === P_GRATE || pj === P_FILTER) continue; if (pj === P_GRATE || pj === P_FILTER) continue;
move(j, i, nx, ny, x, y); move(j, i, nx, ny, x, y);
if (ox !== -dx || oy !== -dy) tubeSpread(i, x, y, -ox, -oy, true);
return; return;
} }
} }
+17 -9
View File
@@ -126,6 +126,22 @@ function machinesTick() {
const x = i % W, y = (i / W) | 0; const x = i % W, y = (i / W) | 0;
// a sideways screw carries its load along the bottom: grains settle into empty lanes below // a sideways screw carries its load along the bottom: grains settle into empty lanes below
if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); continue; } if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); continue; }
if (!flat) {
let own = 0, shifted = false;
const end = dy < 0 ? (y & ~3) : (y | 3);
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
const side = ((tick >> 1) + i) & 1 ? 1 : -1;
for (let c = 0; c < 2; c++) {
const sx = c ? -side : side, xx = x + sx, j = i + sx;
if ((xx >> 2) !== (x >> 2) || part[j] !== p || !roomFor(j) || mat[j] !== EMPTY || occ[j] || stamp[j] === tickStamp) continue;
let load = 0;
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++;
// Include this grain: a strict gap prevents a sideways step being undone next beat.
if (own <= load + 1) continue;
move(i, j, x, y, xx, y); stats.moved++; shifted = true; break;
}
if (shifted) continue;
}
const nx = x + dx, ny = y + dy; const nx = x + dx, ny = y + dy;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue; if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = i + dj; const j = i + dj;
@@ -143,15 +159,7 @@ function machinesTick() {
move(i, j, x, y, nx, ny); stats.moved++; move(i, j, x, y, nx, ny); stats.moved++;
if (tube && pj !== p) { if (tube && pj !== p) {
// turning a corner: keep going across the new tube's width so every lane fills // turning a corner: keep going across the new tube's width so every lane fills
let c = j, cx = nx, cy = ny; tubeSpread(j, nx, ny, dx, dy);
const bxj = nx >> 2, byj = ny >> 2;
for (;;) {
const ex = cx + dx, ey = cy + dy;
if (ex < 0 || ey < 0 || ex >= W || ey >= H || (ex >> 2) !== bxj || (ey >> 2) !== byj) break;
const e = ey * W + ex;
if (part[e] !== pj || !roomFor(e) || occ[e]) break;
move(c, e, cx, cy, ex, ey); c = e; cx = ex; cy = ey;
}
} }
} }
} }
+62 -1
View File
@@ -37,6 +37,66 @@ rect(run, 100, 60, 103, 79, run('WATER'));
ticks(run, 400); ticks(run, 400);
check('water runs down through an upward screw, which never pushes it back', count(run, run('WATER'), 0, 0, 319, 79) === 0, count(run, run('WATER'), 0, 0, 319, 79)); check('water runs down through an upward screw, which never pushes it back', count(run, run('WATER'), 0, 0, 319, 79) === 0, count(run, run('WATER'), 0, 0, 319, 79));
// Intake itself fills the far lanes first, for either tube axis.
for (const horizontal of [false, true]) {
fresh();
run(`placeBlock(BP_TUBE, ${horizontal ? 1 : 0}, 25, 20, true);`);
const filled = run(`(() => {
const lanes = [];
for (let k = 0; k < BS; k++) {
tickStamp++;
const x = ${horizontal ? 100 : 99}, y = ${horizontal ? 79 : 80};
setCell(y*W+x, SAND, AMB[y], 0);
tubeIntake(80*W+100, ${horizontal ? 'P_TR, 1, 0' : 'P_TU, 0, -1'});
for (let lane = 0; lane < BS; lane++) if (mat[(80+${horizontal ? 'lane' : '0'})*W+100+${horizontal ? '0' : 'lane'}] === SAND && !lanes.includes(lane)) lanes.push(lane);
}
return lanes;
})()`);
check(horizontal ? 'a right screw takes grains from above into every row' : 'an up screw takes grains from the side into every column', filled.join() === '3,2,1,0', filled);
}
// A finite heap rides into the side of a six-block lift; remove each mouth flyer once.
const sideSeed = run('RS');
fresh();
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true); for (let by = 19; by <= 24; by++) placeBlock(BP_TUBE, 0, 32, by, true);');
rect(run, 64, 40, 119, 99, run('SAND'));
const sideLift = run(`(() => {
let out = 0; const lanes = [0, 0, 0, 0];
const grains = () => mat.reduce((n, m) => n + (m === SAND), 0) + flyers.filter(f => f.m === SAND).length;
const before = grains();
for (let k = 0; k < 3000; k++) {
simTick();
for (let x = 128; x < 132; x++) if (mat[88*W+x] === SAND) lanes[x-128]++;
for (let f = flyers.length - 1; f >= 0; f--) if (flyers[f].m === SAND && flyers[f].y < 76 && flyers[f].vy < 0) { out++; flyers.splice(f, 1); }
}
return { out, lanes, before, after: grains() + out };
})()`);
console.log('Side-fed lift, 3000 ticks: ' + JSON.stringify(sideLift));
check('a belt feeding a lift from the side fills all four lanes', sideLift.lanes.every(n => n > 0), sideLift.lanes);
check('the side-fed lift conserves its sand, including mouth flyers', sideLift.before === sideLift.after, `${sideLift.before} -> ${sideLift.after}`);
// Before side spreading: 1459 grains left this lift in 3000 ticks (one lane, nearly full).
// The same belt and heap deliver 2243 into an open drop, the most the lift can be given.
check('a side-fed lift carries at least 90% of what its belt brings (it was 65%)', sideLift.out >= Math.floor(0.9 * 2243), sideLift.out);
// Same belt and finite heap, with a non-blocking sink at the tube side.
run(`RS = ${sideSeed}`);
fresh();
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
rect(run, 64, 40, 119, 99, run('SAND'));
const beltDelivery = run(`(() => {
let out = 0;
for (let k = 0; k < 3000; k++) {
simTick();
for (let y = 0; y < H; y++) for (let x = 128; x < W; x++) {
const i = y*W+x;
if (mat[i] === SAND) { out++; setCell(i, EMPTY, AMB[y], 0); }
}
for (let f = flyers.length - 1; f >= 0; f--) if (flyers[f].m === SAND && flyers[f].x >= 128) { out++; flyers.splice(f, 1); }
}
return out;
})()`);
console.log('Belt delivery to a non-blocking sink, 3000 ticks: ' + beltDelivery);
// --- throughput // --- throughput
fresh(); fresh();
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);'); run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
@@ -44,6 +104,7 @@ run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y
rect(run, 80, 0, 95, 79, run('SAND')); rect(run, 80, 0, 95, 79, run('SAND'));
ticks(run, 60); ticks(run, 60);
const lanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()'); const lanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
check('a sideways tube fed from above (its side) fills more than one row', lanes > 1, lanes);
check('a sideways tube fed from a hopper runs full, all four grains wide', lanes === 4, lanes); check('a sideways tube fed from a hopper runs full, all four grains wide', lanes === 4, lanes);
fresh(); fresh();
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);'); run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
@@ -174,7 +235,7 @@ run(`globalThis.__carry = carryColumn;
for (const lift of [true, false]) { for (const lift of [true, false]) {
fresh(); fresh();
run('RS = 7; __shear = 0; for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);'); 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);'); if (lift) run('for (let by = 15; by <= 24; by++) placeBlock(BP_TUBE, 0, 32, by, true);');
else run('for (let by = 10; by <= 25; by++) placeBlock(BP_WALL, RSTONE, 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, 94, 119, 99, run('SAND'));
rect(run, 88, 70, 119, 93, run('CUORE')); rect(run, 88, 70, 119, 93, run('CUORE'));