From c434faefc4b8f62212d462ea6751da8ee2fabddf Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Sat, 24 Oct 2026 00:09:33 -0400 Subject: [PATCH] Screw tubes carry bars, one whole bar at a time Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN.md | 3 + docs/PROJECT_MAP.md | 9 +-- docs/agent-log.md | 23 ++++++++ src/sim/conveyors.js | 1 + src/sim/items.js | 120 ++++++++++++++++++++++++++++++--------- src/sim/machine-tick.js | 2 +- src/sim/pieces.js | 2 +- src/ui/notebook.js | 1 + test/t_conveyors.js | 56 ++++++++++++++++++ tools/scenes/bar-lift.js | 17 ++++++ 10 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 tools/scenes/bar-lift.js diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1dc2135..1ae4351 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -193,6 +193,9 @@ above, including: 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. +- **Screw tubes carry bars**, one whole bar at a time, in line with the grains: a belt pushes a + bar into a tube's side, it rides through corners and leaves the mouth. The owner asked how bars + should be moved and chose tubes over a separate bar lift. ## Ideas not built yet diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 05bf04e..f43ec35 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -248,7 +248,7 @@ 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 (127 lines) +### src/sim/conveyors.js (128 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, tubeSpread, tubeIntake - variables: CARRY_DEPTH, filterR, filterL, filtersDirty @@ -262,9 +262,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 (134 lines) +### src/sim/items.js (200 lines) Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds. -- functions: entSolidCell, entFreeAt, entMark, spawnBar, removeEnt, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText +- functions: entSolidCell, 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) @@ -383,7 +383,7 @@ The settings screen. - functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe - variables: SET_TABS, setTab -### src/ui/notebook.js (201 lines) +### src/ui/notebook.js (202 lines) The notebook (N): milestones, market, wallet, blueprints, controls. - functions: openNotebook, renderNotebook, fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet, renderBlueprintTab, copyCode, controlsHtml - variables: nbTab, MARKET_GROUPS @@ -467,6 +467,7 @@ 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/bar-lift.js`: A mold drops copper bars onto a belt, through a screw lift and onto the upper belt. - `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/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view. diff --git a/docs/agent-log.md b/docs/agent-log.md index 07bcc29..53fc56e 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,29 @@ 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-24: screw tubes carry bars (Claude Code, with Codex) + +Note: "how do you transport bars? belts work but tubes do not, should they?" The owner chose +tubes over a separate bar lift. + +- `src/sim/items.js`: a bar whose eight cells are all tube cells is in the tube (derived from + its position, so saves need nothing new). On the tube beat it steps along the tube, snaps + into the next block's lane at corners, and leaves a mouth with a launch like grains. A bar + pushed by a belt into a tube's side or back, falling into a down tube or sitting on an up + tube's intake enters when the aligned cells are free. Grains and bars wait for each other. + Gas and liquid don't block a bar (Claude added this after review: smoke near furnaces stalled + bars in Codex's version). +- Text: `pieces.js` tube description, `notebook.js`, `docs/DESIGN.md`, `docs/PROJECT_MAP.md`. +- Tests: `t_conveyors` (55 checks): belt into a lift and out the top, right tube, up-to-right + and right-to-down corners, blocked mouth waits without overlap, grains and a bar share a + lift with sand conserved, save mid-tube, smoke and steam in the tube, melting in a tube, + bars still ride and stop on belts and rest on top of sideways tubes. All 11 headless suites + and `play.py` (131) pass; `t_smooth.py`'s half-resolution check failed on the slow fallback + (known flaky). +- Shots: `tools/scenes/bar-lift.js`, `shots/bar-lift-{300,900}.png`: mold, belt, lift, corner, + belt; the bars arrive at the top. +- Open: nothing. + ## 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 diff --git a/src/sim/conveyors.js b/src/sim/conveyors.js index 46a0b31..8324d83 100644 --- a/src/sim/conveyors.js +++ b/src/sim/conveyors.js @@ -106,6 +106,7 @@ function tubeSpread(c, x, y, sx, sy, intake = false) { // 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. function tubeIntake(i, p, dx, dy) { + if (occ[i]) return; const x = i % W, y = (i / W) | 0; const cands = [[-dx, -dy], [dy, dx], [-dy, -dx]]; const start = (tick + i) % 3; diff --git a/src/sim/items.js b/src/sim/items.js index 33e68e1..b1ea1dc 100644 --- a/src/sim/items.js +++ b/src/sim/items.js @@ -32,41 +32,107 @@ function spawnBar(m, x, y, vx, vy, T) { return e; } function removeEnt(e) { entMark(e, 0); const k = ents.indexOf(e); if (k >= 0) ents.splice(k, 1); } +// No transient tube state: even a loaded bar derives its ride from its eight cells. +function entTubeAt(x, y) { + if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return 0; + for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) { + const p = part[yy * W + xx]; if (p < P_TU || p > P_TR) return 0; + } + return part[(y + 1) * W + x + 1]; +} +function entTubeFree(x, y, tubeOnly = false) { + if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false; + for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) { + const j = yy * W + xx, p = part[j], tube = p >= P_TU && p <= P_TR; + // gas and liquid don't stop a bar, in a tube or in the open + if (occ[j] || (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS && PH[mat[j]] !== PH_LIQUID) || (tubeOnly ? !tube : !tube && entSolidCell(j))) return false; + } + return true; +} +// Check the swept rectangle too: a snap must never jump through a grain or another bar. +function entTubeMove(e, x, y, tubeOnly = false) { + if (!entTubeFree(x, y, tubeOnly)) return false; + for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) + for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false; + e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true; +} +function entTubeEnter(e, dx, dy) { + for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) { + const xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k); + if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue; + const p = part[yy * W + xx]; if (p < P_TU || p > P_TR || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue; + // Vertical screws require the full width; horizontal screws use the nearest two-row lane. + const x = (xx & ~3), y = Math.max(yy & ~3, Math.min((yy & ~3) + 2, e.iy)); + if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true; + } + return false; +} +function entTubeTick(e, p) { + e.vx = e.vy = 0; e.x = e.ix; e.y = e.iy; + const ci = (e.iy + 1) * W + e.ix + 1; + if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return; + const dx = TDX[p], dy = TDY[p]; + let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0; + for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) { + const xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k); + if (xx < 0 || yy < 0 || xx >= W || yy >= H) return; + const q = part[yy * W + xx]; + if (q >= P_TU && q <= P_TR && q !== p) { next = q; ax = xx; ay = yy; break; } + } + if (next) { + if (TDX[next] === -dx && TDY[next] === -dy) return; + x = ax & ~3; y = Math.max(ay & ~3, Math.min((ay & ~3) + 2, e.iy)); + if (entTubeMove(e, x, y, true)) stats.moved++; + return; + } + if (entTubeAt(x, y)) { if (entTubeMove(e, x, y, true)) stats.moved++; return; } + // Clear the mouth in one move so the next tick can derive ordinary falling from position. + x = e.ix + dx * BAR_W; y = e.iy + dy * BAR_H; + if (!entTubeFree(x, y) || entTubeAt(x, y)) return; + for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return; + if (entTubeMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; } +} function entTick() { for (let k = ents.length - 1; k >= 0; k--) { const e = ents[k]; entMark(e, 0); entCurM = e.m; const ix = e.ix, iy = e.iy; - // what is underneath? - let belt = 0, bsat = 0, 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) { 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; + const tube = entTubeAt(ix, iy); + if (tube) entTubeTick(e, tube); + else { + // what is underneath? + let belt = 0, bsat = 0, 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) { 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; + } } + 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; } + else { e.vy = Math.min(inLiq ? 0.35 : 1.3, e.vy + 0.07); e.vx *= inLiq ? 0.8 : 0.985; } + if (belt && bsat > 0) stats.moved += 0.05; + e.x += e.vx; e.y += e.vy; + // step toward the new rounded position one cell at a time + let tx = Math.round(e.x), ty = Math.round(e.y), cx = ix, cy = iy; + while (cx !== tx) { const s = Math.sign(tx - cx); if (entFreeAt(cx + s, cy)) cx += s; else if (belt && entTubeEnter(e, s, 0)) { cx = e.ix; cy = e.iy; e.x = cx; e.y = cy; break; } else { e.vx = 0; e.x = cx; break; } } + while (cy !== ty) { const s = Math.sign(ty - cy); if (entTubeAt(cx, cy)) break; if (entFreeAt(cx, cy + s)) cy += s; else if (entTubeEnter(e, 0, s)) { cx = e.ix; cy = e.iy; break; } else { e.vy = 0; e.y = cy; break; } } + if (support && !belt && entTubeEnter(e, 0, 1)) { cx = e.ix; cy = e.iy; } + if (!entTubeAt(cx, cy) && !entFreeAt(cx, cy)) { + // something moved into us (a grain fell in): try to step up out of it + let freed = false; + for (let u = 1; u < 6; u++) if (entFreeAt(cx, cy - u)) { cy -= u; e.y = cy; freed = true; break; } + if (!freed) e.y = cy; + } + e.ix = cx; e.iy = cy; } - 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; } - else { e.vy = Math.min(inLiq ? 0.35 : 1.3, e.vy + 0.07); e.vx *= inLiq ? 0.8 : 0.985; } - if (belt && bsat > 0) stats.moved += 0.05; - e.x += e.vx; e.y += e.vy; - // step toward the new rounded position one cell at a time - let tx = Math.round(e.x), ty = Math.round(e.y), cx = ix, cy = iy; - while (cx !== tx) { const s = Math.sign(tx - cx); if (entFreeAt(cx + s, cy)) cx += s; else { e.vx = 0; e.x = cx; break; } } - while (cy !== ty) { const s = Math.sign(ty - cy); if (entFreeAt(cx, cy + s)) cy += s; else { e.vy = 0; e.y = cy; break; } } - if (!entFreeAt(cx, cy)) { - // something moved into us (a grain fell in): try to step up out of it - let freed = false; - for (let u = 1; u < 6; u++) if (entFreeAt(cx, cy - u)) { cy -= u; e.y = cy; freed = true; break; } - if (!freed) e.y = cy; - } - e.ix = cx; e.iy = cy; + const cx = e.ix, cy = e.iy; // heat: the bar tends toward the temperature around it const T = temp[Math.min(H - 1, cy + 1) * W + Math.min(W - 1, cx + 1)]; e.T += (T - e.T) * 0.04; diff --git a/src/sim/machine-tick.js b/src/sim/machine-tick.js index 1ee237c..2a21068 100644 --- a/src/sim/machine-tick.js +++ b/src/sim/machine-tick.js @@ -113,7 +113,7 @@ function machinesTick() { const list = tubeLists[p], dx = TDX[p], dy = TDY[p], dj = dy * W + dx, flat = dy === 0; for (let k = 0; k < list.length; k++) { const i = list[k]; - if (part[i] !== p) continue; + if (part[i] !== p || occ[i]) continue; const m = mat[i]; if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) tubeIntake(i, p, dx, dy); continue; } if (stamp[i] === tickStamp) continue; diff --git a/src/sim/pieces.js b/src/sim/pieces.js index ba9ef36..1d3b618 100644 --- a/src/sim/pieces.js +++ b/src/sim/pieces.js @@ -42,7 +42,7 @@ const PIECES = [ { id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'], desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it.' }, { id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left'], - desc: 'A turning screw that draws in loose material and pushes it along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Stack them pointing up to lift ore out of a mine, and turn the top one sideways to unload. Copper softens around 1000°C.' }, + desc: 'A turning screw that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Stack them pointing up to lift ore out of a mine, and turn the top one sideways to unload. Copper softens around 1000°C.' }, { id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true, desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through.' }, { id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE, diff --git a/src/ui/notebook.js b/src/ui/notebook.js index 18c8e7f..50a33af 100644 --- a/src/ui/notebook.js +++ b/src/ui/notebook.js @@ -195,6 +195,7 @@ function controlsHtml() { 3. Build (3) a chamber of stone wall blocks with a one-block vent in the roof, a floor of clay Grates, and an Ingot mold right under the grates. The notebook's Blueprints tab has this as a starter design.
4. Tick coal and copper ore in your bag and pick Ticked mix (Q/E, or click it at the bottom of the bag). Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.
5. When the ingot sets, bars drop out of the bottom of the mold onto whatever is under it. Leave one open block under it: a belt below that carries them off, or vacuum them up. Leftover metal stays for the next firing. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.
+ Screw tubes carry grains and whole bars: feed bars in from a belt, turn corners and unload at the mouth.
6. Blow the bars into the trade dock: they sell the moment they drop through its grate, and bars pay far more than ore. Watch the Market page for orders that pay double. Money buys belts, screw tubes and fans; pay down the claim there too.
7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.

`; diff --git a/test/t_conveyors.js b/test/t_conveyors.js index 95d81fa..f604315 100644 --- a/test/t_conveyors.js +++ b/test/t_conveyors.js @@ -250,4 +250,60 @@ for (const lift of [true, false]) { } run('carryColumn = __carry;'); +// --- whole bars in screw tubes; position an existing bar inside for mid-ride cases +function putBar(x, y) { run(`{ spawnBar(BARCU, 20, 40, 0, 0); const e=ents[0]; entMark(e,0); e.x=e.ix=${x}; e.y=e.iy=${y}; entMark(e,1); }`); } +function barTube(corner = false) { + fresh(); + run(`for (let by = 20; by < 24; by++) placeBlock(BP_TUBE, 0, 25, by, true); + ${corner ? 'for (let bx = 25; bx < 29; bx++) placeBlock(BP_TUBE, 1, bx, 19, true);' : ''}`); + putBar(100,94); +} +function watchBar(n, condition) { return run(`(() => { for (let k=0;k<${n};k++) { simTick(); if (${condition}) return true; } return false; })()`); } +fresh(); +run('for (let bx=20;bx<25;bx++) placeBlock(BP_BELT,1,bx,25,true); for(let by=21;by<25;by++) placeBlock(BP_TUBE,0,25,by,true); spawnBar(BARCU,84,98,0,0);'); +check('a belt feeds a whole copper bar into a lift and out its top', watchBar(200, 'ents.length===1 && ents[0].iy+BAR_H<=84'), run('JSON.stringify(ents)')); +fresh(); +run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true);'); putBar(80,82); +check('a whole bar leaves a right tube', watchBar(100,'ents.length===1 && ents[0].ix>=96')); +// steam and smoke drift through tubes near furnaces: a bar passes them as it does in the open +barTube(); +check('steam and smoke in a tube do not stop a bar', run(`(() => { for (let k = 0; k < 120; k++) { + for (let y = 80; y < 94; y++) for (let x = 100; x < 104; x++) if (mat[y*W+x] === EMPTY && !occ[y*W+x]) setCell(y*W+x, (x + y) & 1 ? STEAM : SMOKE, 120, 0); + simTick(); if (ents.length === 1 && ents[0].iy + BAR_H <= 80) return true; } return false; })()`), run('JSON.stringify(ents)')); +barTube(true); +check('a bar turns from an up tube into a right tube and leaves', watchBar(200,'ents.length===1 && ents[0].ix>=116')); +fresh(); +run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true);'); putBar(80,82); +rect(run,96,80,99,83,run('STONE')); +ticks(run,100); +check('a blocked mouth holds the whole bar inside without wall overlap',run('ents.length===1 && ents[0].ix===92 && !mat.some((m,i)=>m===STONE && occ[i])'),run('JSON.stringify(ents)')); +rect(run,96,80,99,83,run('EMPTY')); +check('the bar leaves when the mouth wall is removed',watchBar(80,'ents[0].ix>=96')); +barTube(); +rect(run,100,88,103,89,run('SAND')); +const shared = run(`(() => { let overlap=false, rose=false; const n=mat.reduce((n,m)=>n+(m===SAND),0); for(let k=0;k<160;k++) { simTick(); if(mat.some((m,i)=>m===SAND && occ[i])) overlap=true; if(ents[0].iy<90) rose=true; } return { overlap, rose, n, after:mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length }; })()`); +check('grains and a moving bar share a lift without overlap or lost sand',!shared.overlap && shared.rose && shared.n===shared.after,JSON.stringify(shared)); +barTube(); ticks(run,8); +run('globalThis.__barSave=serializeWorld(); deserializeWorld(__barSave.meta,__barSave.buffers);'); +check('a bar saved mid-tube continues out of its mouth',watchBar(100,'ents.length===1 && ents[0].iy+BAR_H<=80')); +fresh(); +run('for(let bx=20;bx<24;bx++) placeBlock(BP_BELT,1,bx,25,true); placeBlock(BP_WALL,RSTONE,24,25,true); placeBlock(BP_WALL,RSTONE,24,24,true); spawnBar(BARCU,80,98,0,0);'); +ticks(run,100); +check('a normal belt still carries a bar and stops at its blocked end',run('ents.length===1 && ents[0].ix===92 && ents[0].iy===98')); +fresh(); run('placeBlock(BP_TUBE,1,25,25,true); spawnBar(BARCU,100,98,0,0);'); ticks(run,100); +check('a bar resting on a horizontal tube stays on top',run('ents[0].ix===100 && ents[0].iy===98')); + +for (const dir of [0, 2]) { + fresh(); run(`placeBlock(BP_TUBE,${dir},25,20,true); spawnBar(BARCU,100,${dir ? 78 : 84},0,${dir ? 0 : -0.4});`); + check(dir ? 'a falling bar enters a down tube from above' : 'an upward moving bar enters the bottom of an up tube',watchBar(20,'entTubeAt(ents[0].ix,ents[0].iy)!==0')); +} +fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true); for(let by=21;by<25;by++) placeBlock(BP_TUBE,2,23,by,true);'); putBar(80,82); +check('a bar turns from a right tube down into a vertical lane',watchBar(160,'ents[0].iy>=100')); +barTube(true); rect(run,100,76,103,79,run('STONE')); ticks(run,100); +check('a blocked corner waits without overlapping stone',run('ents[0].iy===80 && !mat.some((m,i)=>m===STONE && occ[i])')); +rect(run,100,76,103,79,run('EMPTY')); +check('a blocked corner resumes when cleared',watchBar(120,'ents[0].ix>=116')); +barTube(); run('ents[0].T=1200; for(let y=94;y<96;y++) for(let x=100;x<104;x++) temp[y*W+x]=1200; entTick();'); +check('a bar melting inside a tube becomes eight molten grains',run('ents.length===0') && count(run,run('MCU'))===8); + done(); diff --git a/tools/scenes/bar-lift.js b/tools/scenes/bar-lift.js new file mode 100644 index 0000000..149252b --- /dev/null +++ b/tools/scenes/bar-lift.js @@ -0,0 +1,17 @@ +// A mold drops copper bars onto a belt, through a screw lift and onto the upper belt. +money = 50000; +const bx = Math.floor((SX - 100) / BS), by = Math.floor((SY + 24) / BS); +for (let y = (by - 18) * BS; y < (by + 4) * BS; y++) for (let x = (bx - 3) * BS; x < (bx + 27) * BS; x++) { + bgm[y * W + x] = 0; setCell(y * W + x, EMPTY, AMB[y], 0); +} +for (let x = (bx - 3) * BS; x < (bx + 27) * BS; x++) surfY[x] = (by + 4) * BS; +const mo = addMold('ingot', bx, by - 4, RSTONE, true); +const beltY = Math.ceil((mo.y + mo.h) / BS) + 1; +for (let x = bx; x < bx + 9; x++) placeBlock(BP_BELT, 1, x, beltY, true); +for (let y = by - 11; y < beltY; y++) placeBlock(BP_TUBE, 0, bx + 9, y, true); +for (let x = bx + 9; x < bx + 16; x++) placeBlock(BP_TUBE, 1, x, by - 12, true); +for (let x = bx + 16; x < bx + 25; x++) placeBlock(BP_BELT, 1, x, by - 11, true); +placeBlock(BP_WALL, RSTONE, bx + 25, by - 12, true); +mo.cavs[0].forEach(i => setCell(i, MCU, 1090, 0)); +window.CAM = [(bx + 12) * BS, (by - 6) * BS]; +updateRoof();