Screw conveyors join by the shared rule: low ends and verticals join in the corner block (t67 phase 2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 13:30:44 -04:00
co-authored by Claude Opus 5.5
parent 198bea54da
commit fd35bf334f
14 changed files with 358 additions and 209 deletions
+15 -13
View File
@@ -123,8 +123,10 @@ dist/, shots/ build output and screenshots (not committed)
`endJoin(bx, by, endIndex, planned = null)` returns `neighbourBlock * 2 + neighbourEnd`, `endJoin(bx, by, endIndex, planned = null)` returns `neighbourBlock * 2 + neighbourEnd`,
or -1; `pieceJoins(bx, by, planned = null)` returns bits 0/1 for the joined ends. or -1; `pieceJoins(bx, by, planned = null)` returns bits 0/1 for the joined ends.
Plans override world blocks, including the queried block. Continuing diagonals take priority Plans override world blocks, including the queried block. Continuing diagonals take priority
over elbows; a join is returned only when both ends choose each other. Belt drawing uses over elbows; a join is returned only when both ends choose each other. Belt and screw drawing
this module; screw drawing and movement still use their existing rules. use this module, as do diagonal screw grain and bar transfers (`diagonalScrewNext` and
`straightScrewNext`). Straight screws still feed the side of lifts. E elbows bend in the
straight end block; both looks, ghosts and previews share that geometry.
- **Placing and removing:** - **Placing and removing:**
- `placeBlock` / `removeBlock` (`blocks.js`); - `placeBlock` / `removeBlock` (`blocks.js`);
- `placeMachineAt` / `addMachine` / `removeMachine` (`actions.js`, `machines.js`); - `placeMachineAt` / `addMachine` / `removeMachine` (`actions.js`, `machines.js`);
@@ -141,7 +143,7 @@ dist/, shots/ build output and screenshots (not committed)
- **The pixel look:** - **The pixel look:**
- `drawWorld` (`src/render/world.js`) fills one pixel per grain; - `drawWorld` (`src/render/world.js`) fills one pixel per grain;
- `diagonal-screws.js` shares screw join and casing geometry between pixels, smooth shading tags and previews; - `diagonal-screws.js` derives diagonal and straight end bends from `endJoin`, sharing casing geometry between pixels, smooth shading tags, ghosts and previews;
- `computeLight` (`light.js`) adds coloured light; - `computeLight` (`light.js`) adds coloured light;
- overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards - overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards
(`machines.js`, `overlays.js`). (`machines.js`, `overlays.js`).
@@ -277,9 +279,9 @@ Geometric transport ends. Tables are built once; querying a world or plan alloca
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins - functions: pieceEnds, joinCandidate, endJoin, pieceJoins
- variables: JOIN_ENDS - variables: JOIN_ENDS
### src/sim/conveyors.js (198 lines) ### src/sim/conveyors.js (223 lines)
How belts, filters and screw conveyors 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 conveyors 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: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, diagonalScrewNext - functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, screwAccepts, straightScrewNext, diagonalScrewNext
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty - variables: CARRY_DEPTH, filterR, filterL, filtersDirty
### src/sim/slope-conveyors.js (230 lines) ### src/sim/slope-conveyors.js (230 lines)
@@ -296,7 +298,7 @@ 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 - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord - variables: DROSS, grains, barWord
### src/sim/items.js (288 lines) ### src/sim/items.js (307 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.
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText - functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
- variables: BAR_W, BAR_H, entCurM, droppedLast - variables: BAR_W, BAR_H, entCurM, droppedLast
@@ -315,7 +317,7 @@ Trees (background layer), acorns, saplings, and waking freezing water.
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree - functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
- variables: treeBases, saplings, treeDraw - variables: treeBases, saplings, treeDraw
### src/sim/vacuum.js (179 lines) ### src/sim/vacuum.js (199 lines)
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain. The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite - functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
- variables: blowRR, FLY_G, FLY_Q - variables: blowRR, FLY_G, FLY_Q
@@ -379,12 +381,12 @@ Drawing only: the face stays on its simulation cells; the band hangs below it.
- functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat - functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat
- variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner - variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner
### src/render/diagonal-screws.js (68 lines) ### src/render/diagonal-screws.js (78 lines)
Screw casing geometry shared by both looks, ghosts and placement previews. smooth-look class of a screw conveyor cell by direction: 9 up, 10 right, 11 down, 12 left Screw casing geometry shared by both looks, ghosts and placement previews. smooth-look class of a screw conveyor cell by direction: 9 up, 10 right, 11 down, 12 left
- functions: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock - functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
### src/render/world.js (403 lines) ### src/render/world.js (404 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld - functions: fogColor, drawWorld
@@ -405,7 +407,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js. Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
- variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW - variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW
### src/render/smooth-shaders.js (370 lines) ### src/render/smooth-shaders.js (377 lines)
Smooth shaders: vertex and fragment programs for the rounded world look. Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS - variables: SM_VS, SM_FS
@@ -498,7 +500,7 @@ Gamepad input.
- functions: padPressed, pollPad - functions: padPressed, pollPad
- variables: PAD - variables: PAD
### src/ui/previews.js (239 lines) ### src/ui/previews.js (240 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch. Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock - functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
@@ -580,6 +582,7 @@ The frame loop and boot.
- `tools/scenes/bar-lift.js`: A mold drops copper bars onto a belt, through a screw lift and onto the upper belt. - `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/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/joins.js`: Shared join grid: eight rows (UR/DR/DL/UL, left/right ends), nine columns. Screw H and V, then belt H; each tries the side, above/below, and E. Self-contained, paused and empty. Use --zoom 4 --viewport 2100x1500 --no-light.
- `tools/scenes/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view. - `tools/scenes/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view.
- `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge. - `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
- `tools/scenes/slope-conveyors.js`: Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen). - `tools/scenes/slope-conveyors.js`: Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen).
@@ -590,4 +593,3 @@ The frame loop and boot.
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect. - `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
- `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand. - `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
- `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below. - `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
- `tools/scenes/t62-joins.js`: Self-contained grid. Rows: UR, DR, DL, UL. Each run shows both ends. Columns: horizontal edge, horizontal corner, vertical edge, vertical corner. Corner columns stay unconnected, with two separate ends. Repeat those four with the straight travel reversed. Camera fits --zoom 6 --viewport 2100x1300. Run after clear-sky.js or by itself. Empty, paused screws expose the whole casing.
+19
View File
@@ -4,6 +4,25 @@ 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-27: t67 phase 2, screw conveyors follow the join rule (Claude Code + Codex)
- Screw drawing, grain flow and bars now all go through `endJoin`. New joins: a horizontal in
E at a diagonal's low end, and a vertical in E at either end; both draw an elbow that bends
inside the straight piece's end block. Same-row high-end elbows are unchanged.
- No longer joined (moves nothing): a same-row horizontal at a low end, a vertical directly
above or below a diagonal's last block, and any other side contact. Joined belts and filters
take discharge on their carrying face at either end. A horizontal screw feeding the side of a
vertical lift still works (`straightScrewNext` keeps straight-to-straight transfers).
- The opposite-direction check is one helper, `screwAccepts`.
- Files: `src/render/diagonal-screws.js`, `src/render/smooth-shaders.js`, `src/render/world.js`,
`src/ui/previews.js`, `src/sim/conveyors.js`, `src/sim/machine-tick.js`, `src/sim/items.js`,
`src/sim/cells.js`, `tools/scenes/joins.js` (replaces `t62-joins.js`), `test/t_diag_screws.js`,
`test/t_smooth.py`, `docs/PROJECT_MAP.md`.
- Tests: `python3 test/run_all.py --browser` all passed; determinism diff changed only screw
checks. Screenshots: `shots/joins-*.png`.
- Open: inside the new elbows the flight stripes of both pieces overlap, which looks a little
busy in the smooth look. Layouts built on the removed joins need moving.
## 2026-10-27: t63 vacuum picks up grains in mid-air (Claude Code + Codex) ## 2026-10-27: t63 vacuum picks up grains in mid-air (Claude Code + Codex)
- `toolVacuum` now takes flyers (grains a screw launches or that were blown out) inside its - `toolVacuum` now takes flyers (grains a screw launches or that were blown out) inside its
+36 -26
View File
@@ -5,26 +5,30 @@ for (let p = P_SCREW_UR; p <= P_SCREW_UL; p++) SCREW_CLASS[p] = 19 + p - P_SCREW
// Drawing only: the four transport lanes remain in the square block. // Drawing only: the four transport lanes remain in the square block.
let diagonalJoins = new Uint8Array(0); let diagonalJoins = new Uint8Array(0);
function diagonalScrewJoins(bx, by, v, planned = null) { function diagonalScrewJoins(bx, by, v, planned = null) {
// Three bits per end: 1..4 edge exits; bits 6/7 are collars. // Three bits per end: 1..4 edge exits, 5 horizontal in E, 6 vertical in E; bits 6/7 collars.
const dy = v === 4 || v === 6 ? -1 : 1;
let joins = 0; let joins = 0;
for (let end = 0; end < 2; end++) { for (let end = 0; end < 2; end++) {
const side = end ? 1 : -1, nx = bx + side, ny = by + side * dy; const join = endJoin(bx, by, end, planned);
const nb = ny * BW + nx, next = planned && planned.get(nb); if (join < 0) { joins |= 64 << end; continue; }
const nc = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb]; const nb = join >> 1, next = planned && planned.get(nb);
if (nx >= 0 && ny >= 0 && nx < BW && ny < BH && const code = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb];
nc === BP_SCREW && nv >= 4 && (nv === 4 || nv === 6) === (dy < 0)) continue; if (pieceEnds(code, nv)[join & 1].diagonal) continue;
let join = 0; const horizontal = code !== BP_SCREW || (nv & 1);
for (const dir of [side > 0 ? 1 : 3, side * dy > 0 ? 2 : 0]) { const kind = ((nb / BW) | 0) === by ? (end ? 2 : 4) : horizontal ? 5 : 6;
const x = bx + SCREW_DIRS[dir][0], y = by + SCREW_DIRS[dir][1]; joins |= kind << (end * 3);
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
const n = y * BW + x, next = planned && planned.get(n);
const code = next ? next[0] : blockP[n], nv = next ? next[1] : blockV[n];
if ((code === BP_SCREW && nv < 4 && (nv & 1) === (dir & 1)) ||
((code === BP_BELT || code === BP_FILTER) && (dir & 1))) { join = dir + 1; break; }
} }
joins |= join << (end * 3); return joins;
if (!join) joins |= 64 << end; }
// Only E elbows bend a straight block: 1 rising, 2 falling, three bits per end.
function straightScrewJoins(bx, by, v, planned = null) {
let joins = 0;
for (let end = 0; end < 2; end++) {
const join = endJoin(bx, by, end, planned);
if (join < 0) continue;
const nb = join >> 1, next = planned && planned.get(nb);
const code = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb];
if (code === BP_SCREW && nv >= 4 && ((nb / BW) | 0) !== by && nb % BW !== bx)
joins |= (nv === 4 || nv === 6 ? 1 : 2) << (end * 3);
} }
return joins; return joins;
} }
@@ -35,23 +39,28 @@ function diagonalScrewShape(x, y, b, v, joins) {
let best = Infinity; let best = Infinity;
for (let end = 0; end < 2; end++) { for (let end = 0; end < 2; end++) {
const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1; const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1;
const slope = v === 4 || v === 6 ? -1 : 1; const slope = v === 4 || v === 6 ? -1 : 1, straight = v < 4, vert = straight && !(v & 1);
const dx = join ? SCREW_DIRS[join - 1][0] : side * Math.SQRT1_2; const bent = straight && join;
const dy = join ? SCREW_DIRS[join - 1][1] : side * slope * Math.SQRT1_2; const diagonal = straight ? bent : !join || join >= 5;
const grade = straight ? (join === 1 ? -1 : 1) : slope;
const dx = diagonal ? side * (vert ? grade : 1) * Math.SQRT1_2 : straight ? (vert ? 0 : side) : SCREW_DIRS[join - 1][0];
const dy = diagonal ? side * (vert ? 1 : grade) * Math.SQRT1_2 : straight ? (vert ? side : 0) : SCREW_DIRS[join - 1][1];
const along = lx * dx + ly * dy, across = -lx * dy + ly * dx; const along = lx * dx + ly * dy, across = -lx * dy + ly * dx;
const radius = Math.hypot(across, Math.min(0, along)); const radius = Math.hypot(across, Math.min(0, along));
const cap = along - (join ? 2 : 2 * Math.SQRT2); const cap = along - (diagonal ? 2 * Math.SQRT2 : 2);
const sd = Math.max(radius - 2, join || (joins & (64 << end)) ? cap : -10); const sd = Math.max(radius - 2, !straight && (joins & (64 << end)) ? cap : -10);
if (sd < best) { if (sd < best) {
best = sd; diagonalAcross = side * across < 0 ? -radius : radius; best = sd; diagonalAcross = side * across < 0 ? -radius : radius;
diagonalAlong = ((b % BW) * BS + 2 + (((b / BW) | 0) * BS + 2) * (v === 4 || v === 6 ? -1 : 1)) * Math.SQRT1_2 + side * along; diagonalCap = joins & (64 << end) ? cap : -10; const ox = (b % BW) * BS + 2, oy = ((b / BW) | 0) * BS + 2;
diagonalAlong = (straight ? (vert ? oy : ox) : (ox + oy * slope) * Math.SQRT1_2) + side * along;
diagonalCap = joins & (64 << end) ? cap : -10;
} }
} }
return best; return best;
} }
function diagonalScrewBlock(x, y, smooth) { function diagonalScrewBlock(x, y, smooth) {
const i = y * W + x, own = (y >> 2) * BW + (x >> 2); const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
if (part[i] >= P_SCREW_UR && part[i] <= P_SCREW_UL) return own; if (IS_SCREW[part[i]] && (blockV[own] >= 4 || diagonalJoins[own])) return own;
if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1; if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1;
let best = smooth ? 0.75 : 0, found = -1; let best = smooth ? 0.75 : 0, found = -1;
for (let k = 0; k < 4; k++) { for (let k = 0; k < 4; k++) {
@@ -59,8 +68,9 @@ function diagonalScrewBlock(x, y, smooth) {
const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0); const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0);
if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue; if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
const b = by * BW + bx, v = blockV[b]; const b = by * BW + bx, v = blockV[b];
if (blockP[b] !== BP_SCREW || v < 4) continue; if (blockP[b] !== BP_SCREW || (v < 4 && !diagonalJoins[b])) continue;
const joins = diagonalJoins[b] === undefined ? diagonalScrewJoins(bx, by, v) : diagonalJoins[b]; const joins = diagonalJoins[b] === undefined ? (v >= 4 ? diagonalScrewJoins(bx, by, v) : straightScrewJoins(bx, by, v)) : diagonalJoins[b];
if (v < 4 && ((v & 1) ? (x >> 2) !== bx : (y >> 2) !== by)) continue;
const sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins); const sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
if (sd <= best) { best = sd; found = b; } if (sd <= best) { best = sd; found = b; }
} }
+15 -8
View File
@@ -255,29 +255,36 @@ ${SM_SLOPE_DRAW}
float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0; float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0;
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x; float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
float screwSD = -1.0, capRing = 0.0; float screwSD = -1.0, capRing = 0.0;
if (nc >= 19) { vec4 screwTag = cellAt(tTree, c);
int screwJoins = int(screwTag.a * 255.0 + 0.5);
// Straight bend tags use six bits; background trees use 128/255.
bool shaped = nc >= 19 || (screwJoins > 0 && screwJoins < 64);
if (shaped) {
vec2 d = vec2(1.0, nc == 19 || nc == 21 ? -1.0 : 1.0); vec2 d = vec2(1.0, nc == 19 || nc == 21 ? -1.0 : 1.0);
vec4 tag = cellAt(tTree, c); vec4 tag = screwTag;
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w) - 2.0; vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w) - 2.0;
int joins = int(tag.a * 255.0 + 0.5); int joins = screwJoins;
screwSD = 100.0; screwSD = 100.0;
for (int end = 0; end < 2; end++) { for (int end = 0; end < 2; end++) {
int join = (joins >> (end * 3)) & 7; int join = (joins >> (end * 3)) & 7;
float side = end == 0 ? -1.0 : 1.0; float side = end == 0 ? -1.0 : 1.0;
vec2 ray = join == 0 ? vec2(side, side * (nc == 19 || nc == 21 ? -1.0 : 1.0)) * 0.70710678 : bool diagonal = nc >= 19 ? join == 0 || join >= 5 : join != 0;
float grade = nc >= 19 ? d.y : join == 1 ? -1.0 : 1.0;
vec2 ray = diagonal ? (vert ? vec2(side * grade, side) : vec2(side, side * grade)) * 0.70710678 :
nc < 19 ? (vert ? vec2(0, side) : vec2(side, 0)) :
join == 1 ? vec2(0, -1) : join == 2 ? vec2(1, 0) : join == 3 ? vec2(0, 1) : vec2(-1, 0); join == 1 ? vec2(0, -1) : join == 2 ? vec2(1, 0) : join == 3 ? vec2(0, 1) : vec2(-1, 0);
float t = dot(local, ray), cross2 = dot(local, vec2(-ray.y, ray.x)); float t = dot(local, ray), cross2 = dot(local, vec2(-ray.y, ray.x));
float radius = length(vec2(cross2, min(0.0, t))), cap = t - (join == 0 ? 2.82842712 : 2.0); float radius = length(vec2(cross2, min(0.0, t))), cap = t - (diagonal ? 2.82842712 : 2.0);
// A connected mouth meets another opaque pipe; fading its edge leaves a seam. // A connected mouth meets another opaque pipe; fading its edge leaves a seam.
float sd = max(radius - 2.0, (joins & (64 << end)) != 0 ? cap : -10.0); float sd = max(radius - 2.0, (joins & (64 << end)) != 0 ? cap : -10.0);
if (sd < screwSD) { if (sd < screwSD) {
screwSD = sd; screwSD = sd;
a = 2.0 + (side * cross2 < 0.0 ? -radius : radius); a = 2.0 + (side * cross2 < 0.0 ? -radius : radius);
along = dot(w - local, d) * 0.70710678 + side * t; along = (nc >= 19 ? dot(w - local, d) * 0.70710678 : vert ? w.y - local.y : w.x - local.x) + side * t;
capRing = (joins & (64 << end)) != 0 ? smoothstep(-0.55, -0.15, cap) : 0.0; capRing = (joins & (64 << end)) != 0 ? smoothstep(-0.55, -0.15, cap) : 0.0;
} }
} }
sp2 = nc == 19 || nc == 20 ? 1.0 : -1.0; if (nc >= 19) sp2 = nc == 19 || nc == 20 ? 1.0 : -1.0;
} }
float wall = 0.62; float wall = 0.62;
vec3 tc; vec3 tc;
@@ -298,7 +305,7 @@ ${SM_SLOPE_DRAW}
tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8)); tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8));
tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0)); tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0));
} }
if (nc >= 19) { if (shaped) {
bool full = own.r + own.g + own.b > 0.0; bool full = own.r + own.g + own.b > 0.0;
if (full) C = mix(C, own.rgb, 1.0 - smoothstep(0.36, 0.5, length(fract(w) - 0.5))); if (full) C = mix(C, own.rgb, 1.0 - smoothstep(0.36, 0.5, length(fract(w) - 0.5)));
C = mix(C, tc, 1.0 - smoothstep(2.0 - px, 2.0 + px, screwSD + 2.0)); C = mix(C, tc, 1.0 - smoothstep(2.0 - px, 2.0 + px, screwSD + 2.0));
+5 -4
View File
@@ -24,7 +24,7 @@ function drawWorld() {
for (let by = Math.max(0, (vy0 >> 2) - 1); by <= Math.min(BH - 1, ((vy0 + bh) >> 2) + 1); by++) { for (let by = Math.max(0, (vy0 >> 2) - 1); by <= Math.min(BH - 1, ((vy0 + bh) >> 2) + 1); by++) {
for (let bx = Math.max(0, (vx0 >> 2) - 1); bx <= Math.min(BW - 1, ((vx0 + bw) >> 2) + 1); bx++) { for (let bx = Math.max(0, (vx0 >> 2) - 1); bx <= Math.min(BW - 1, ((vx0 + bw) >> 2) + 1); bx++) {
const b = by * BW + bx; const b = by * BW + bx;
if (blockP[b] === BP_SCREW && blockV[b] >= 4) diagonalJoins[b] = diagonalScrewJoins(bx, by, blockV[b]); if (blockP[b] === BP_SCREW) diagonalJoins[b] = blockV[b] >= 4 ? diagonalScrewJoins(bx, by, blockV[b]) : straightScrewJoins(bx, by, blockV[b]);
} }
} }
prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh); prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh);
@@ -179,7 +179,7 @@ function drawWorld() {
const vert = p === P_SCREW_U || p === P_SCREW_D; const vert = p === P_SCREW_U || p === P_SCREW_D;
const axis = diagonal ? Math.floor(diagonalAlong) : vert ? y : x; const axis = diagonal ? Math.floor(diagonalAlong) : vert ? y : x;
const across = diagonal ? diagonalAcross + 2 : vert ? x & 3 : y & 3; const across = diagonal ? diagonalAcross + 2 : vert ? x & 3 : y & 3;
const dir = diagonal ? (blockV[db] >= 6 ? 1 : -1) : (p === P_SCREW_U || p === P_SCREW_L) ? 1 : -1; const dir = diagonal && blockV[db] >= 4 ? (blockV[db] >= 6 ? 1 : -1) : (p === P_SCREW_U || p === P_SCREW_L) ? 1 : -1;
if (diagonal ? Math.abs(diagonalAcross) > 1.38 || diagonalCap > -0.55 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through if (diagonal ? Math.abs(diagonalAcross) > 1.38 || diagonalCap > -0.55 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; } if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; } if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
@@ -339,12 +339,13 @@ function drawWorld() {
let tr = 90, tg = 190, tb = 230; let tr = 90, tg = 190, tb = 230;
if (blocked) { tr = 230; tg = 70; tb = 60; } if (blocked) { tr = 230; tg = 70; tb = 60; }
else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; } else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && v >= 4; const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && (v >= 4 || straightScrewJoins(gbx, gby, v, ghostBlocks));
const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : 0; const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? (v >= 4 ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : straightScrewJoins(gbx, gby, v, ghostBlocks)) : 0;
const caps = code === BP_BELT || code === BP_FILTER ? flatBeltCaps(b, ghostBlocks) : 3; const caps = code === BP_BELT || code === BP_FILTER ? flatBeltCaps(b, ghostBlocks) : 3;
for (let dy = slope ? -9 : diagonal ? -4 : 0; dy < (slope || diagonal ? 8 : BS); dy++) for (let dx = slope ? -5 : diagonal ? -4 : 0; dx < (slope ? 9 : diagonal ? 8 : BS); dx++) { for (let dy = slope ? -9 : diagonal ? -4 : 0; dy < (slope || diagonal ? 8 : BS); dy++) for (let dx = slope ? -5 : diagonal ? -4 : 0; dx < (slope ? 9 : diagonal ? 8 : BS); dx++) {
let sd = 0; let sd = 0;
if (diagonal) { if (diagonal) {
if (v < 4 && ((v & 1) ? dx < 0 || dx >= BS : dy < 0 || dy >= BS)) continue;
const x = gbx * BS + dx, y = gby * BS + dy; const x = gbx * BS + dx, y = gby * BS + dy;
if (x < 0 || y < 0 || x >= W || y >= H) continue; if (x < 0 || y < 0 || x >= W || y >= H) continue;
const i = y * W + x, n = (y >> 2) * BW + (x >> 2); const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
+2 -2
View File
@@ -44,7 +44,7 @@ function enterFree(m, ph, j, dx, dy) {
if (occ[j]) return 0; if (occ[j]) return 0;
const p = part[j]; const p = part[j];
if (p !== 0) { if (p !== 0) {
if (IS_SCREW[p]) { if (ph === PH_POWDER && SCREW_DX[p] === -dx && SCREW_DY[p] === -dy) return 0; } // liquids and gases flow through freely if (IS_SCREW[p]) { if (ph === PH_POWDER && !screwAccepts(p, dx, dy)) return 0; } // liquids and gases flow through freely
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return 0; } // thick slag does not run through a grate else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return 0; } // thick slag does not run through a grate
else if (p === P_INTAKE) { if (ph !== PH_GAS) return 0; } else if (p === P_INTAKE) { if (ph !== PH_GAS) return 0; }
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return 0; } else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return 0; }
@@ -110,7 +110,7 @@ function passable(m, ph, j, dx, dy) {
if (occ[j]) return false; if (occ[j]) return false;
const p = part[j]; const p = part[j];
if (p !== 0) { if (p !== 0) {
if (IS_SCREW[p]) { if (ph === PH_POWDER && SCREW_DX[p] === -dx && SCREW_DY[p] === -dy) return false; } if (IS_SCREW[p]) { if (ph === PH_POWDER && !screwAccepts(p, dx, dy)) return false; }
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return false; } else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return false; }
else if (p === P_INTAKE) { if (ph !== PH_GAS) return false; } else if (p === P_INTAKE) { if (ph !== PH_GAS) return false; }
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return false; } else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return false; }
+38 -13
View File
@@ -176,23 +176,48 @@ function screwIntake(i, p, dx, dy) {
} }
} }
// Four lanes cross a block in four beats; the casing carries every lane across the corner. // Powder may enter any screw except one pushing exactly against it.
// Prefer the next diagonal block, then either straight join, then the open head. function screwAccepts(p, dx, dy) {
return SCREW_DX[p] !== -dx || SCREW_DY[p] !== -dy;
}
// Straight mouths retain side-feeding into lifts; only diagonal transfers need a join.
function straightScrewNext(i, p) {
const dx = SCREW_DX[p], dy = SCREW_DY[p], x = i % W, y = (i / W) | 0;
const xx = x + dx, yy = y + dy, next = yy * W + xx;
if ((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) return next;
const end = dx + dy > 0 ? 1 : 0;
const q = part[next], nb = (yy >> 2) * BW + (xx >> 2);
if (blockP[nb] && !(SCREW_DX[q] && SCREW_DY[q])) return next;
const join = endJoin(x >> 2, y >> 2, end);
if (join >= 0) {
const target = join >> 1, v = blockV[target];
if (blockP[target] === BP_SCREW && v >= 4) {
if (target === nb) return next;
if (!blockP[nb]) {
const lane = dx ? y & 3 : x & 3;
const tx = target % BW * BS + (join & 1 ? BS - 1 : 0);
return (((target / BW) | 0) * BS + lane) * W + tx;
}
}
}
return SCREW_DX[q] && SCREW_DY[q] ? -1 : next;
}
// Four lanes cross a block in four beats; only a mutual geometric join can receive them.
function diagonalScrewNext(i, p) { function diagonalScrewNext(i, p) {
const x = i % W, y = (i / W) | 0, dx = SCREW_DX[p], dy = SCREW_DY[p]; const x = i % W, y = (i / W) | 0, dx = SCREW_DX[p], dy = SCREW_DY[p];
const xx = x + dx, bx = x & ~3, by = y & ~3; const xx = x + dx;
if (xx < 0 || xx >= W) return -1; if (xx < 0 || xx >= W) return -1;
if ((xx >> 2) === (x >> 2)) return i + dx; if ((xx >> 2) === (x >> 2)) return i + dx;
const yy = y + dy * BS; const yy = y + dy * BS, join = endJoin(x >> 2, y >> 2, dx > 0 ? 1 : 0);
if (yy >= 0 && yy < H) { if (join >= 0) {
const q = part[yy * W + xx]; const nb = join >> 1, nx = nb % BW * BS, ny = ((nb / BW) | 0) * BS;
if (q >= P_SCREW_UR && q <= P_SCREW_UL && SCREW_DX[q] === dx && SCREW_DY[q] === dy) return yy * W + xx; if (blockP[nb] === BP_BELT || blockP[nb] === BP_FILTER) return (ny - 1) * W + xx;
if (blockP[nb] === BP_SCREW) {
const v = blockV[nb];
if (v >= 4) return yy * W + xx;
if (v & 1) return (ny + (y & 3)) * W + xx;
return (ny + (dy > 0 ? 0 : 3)) * W + nx + (y & 3);
}
} }
if (IS_SCREW[part[y * W + xx]]) return y * W + xx;
if (dy > 0 && yy < H && (mat[yy * W + xx] === BELTL || mat[yy * W + xx] === BELTR)) return ((yy & ~3) - 1) * W + xx;
const hy = by + (dy > 0 ? BS : -1), hx = bx + (y & 3);
if (hy >= 0 && hy < H && IS_SCREW[part[hy * W + hx]]) return hy * W + hx;
// An open mouth discharges along the diagonal, clear of the last block.
// A corner screw is a separate end, not an open discharge cell.
return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1; return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1;
} }
+28 -9
View File
@@ -90,16 +90,32 @@ function entScrewTick(e, p) {
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return; if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
const dx = SCREW_DX[p], dy = SCREW_DY[p]; const dx = SCREW_DX[p], dy = SCREW_DY[p];
if (dx && dy) { entDiagonalScrewTick(e, p); return; } if (dx && dy) { entDiagonalScrewTick(e, p); return; }
const frontX = e.ix + (dx > 0 ? BAR_W - 1 : 0), frontY = e.iy + (dy > 0 ? BAR_H - 1 : 0);
const front = frontY * W + frontX, dest = straightScrewNext(front, p);
if (dest < 0) return;
if (dest !== front + dx + dy * W) {
const q = part[dest];
if (!screwAccepts(q, dx, dy)) return;
const x = (dest % W) & ~3, y = entScrewLane((dest / W) | 0, e.iy);
if (entScrewMove(e, x, y, true)) stats.moved++;
return;
}
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0; 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++) { for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const i = entScrewEdge(e, dx, dy, k); const i = entScrewEdge(e, dx, dy, k);
if (i < 0) return; if (i < 0) return;
const xx = i % W, yy = (i / W) | 0; const xx = i % W, yy = (i / W) | 0;
const q = part[i]; const q = part[i];
if (IS_SCREW[q] && q !== p) { next = q; ax = xx; ay = yy; break; } if (IS_SCREW[q] && q !== p) {
if (SCREW_DX[q] && SCREW_DY[q]) {
const join = endJoin(frontX >> 2, frontY >> 2, dx + dy > 0 ? 1 : 0);
if (join < 0 || (join >> 1) !== (yy >> 2) * BW + (xx >> 2)) return;
}
next = q; ax = xx; ay = yy; break;
}
} }
if (next) { if (next) {
if (SCREW_DX[next] === -dx && SCREW_DY[next] === -dy) return; if (!screwAccepts(next, dx, dy)) return;
x = ax & ~3; y = entScrewLane(ay, e.iy); x = ax & ~3; y = entScrewLane(ay, e.iy);
if (entScrewMove(e, x, y, true)) stats.moved++; if (entScrewMove(e, x, y, true)) stats.moved++;
return; return;
@@ -115,17 +131,20 @@ function entScrewTick(e, p) {
function entDiagonalScrewTick(e, p) { function entDiagonalScrewTick(e, p) {
if (tick & 7) return; if (tick & 7) return;
const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3; const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
for (const [ox, oy] of [[dx, dy], [dx, 0], [0, dy]]) { const edge = (by + lane) * W + bx + (dx > 0 ? BS - 1 : 0);
const x = bx + ox * BS, y = by + oy * BS + lane, q = entScrewAt(x, y); const dest = diagonalScrewNext(edge, p);
if (!q || (ox && oy && (q < P_SCREW_UR || q > P_SCREW_UL || SCREW_DX[q] !== dx || SCREW_DY[q] !== dy))) continue; if (dest < 0) return;
const x = (dest % W) & ~3;
let y = (dest / W) | 0;
const q = part[dest];
if (IS_SCREW[q]) {
if (SCREW_DX[q] * dx + SCREW_DY[q] * dy < 0) return; if (SCREW_DX[q] * dx + SCREW_DY[q] * dy < 0) return;
y = Math.min((y & ~3) + BS - BAR_H, y);
if (entScrewMove(e, x, y, true)) stats.moved++; if (entScrewMove(e, x, y, true)) stats.moved++;
return; return;
} }
const x = bx + dx * BS; const join = endJoin(bx >> 2, by >> 2, dx > 0 ? 1 : 0);
let y = by + dy * BS + lane; if (join >= 0 && (blockP[join >> 1] === BP_BELT || blockP[join >> 1] === BP_FILTER)) y -= BAR_H - 1;
const belt = y >= 0 && y < H ? mat[y * W + x] : EMPTY;
if (dy > 0 && (belt === BELTL || belt === BELTR)) y = (y & ~3) - BAR_H;
if (!entScrewFree(x, y)) return; if (!entScrewFree(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; 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 (entScrewMove(e, x, y, false, dy < 0)) { e.vx = dx * 0.5; e.vy = dy * 0.6; stats.moved++; } if (entScrewMove(e, x, y, false, dy < 0)) { e.vx = dx * 0.5; e.vy = dy * 0.6; stats.moved++; }
+3 -3
View File
@@ -97,7 +97,7 @@ function machinesTick() {
// beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt // beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt
if ((tick & 1) === 0) for (let p = 1; p < IS_SCREW.length; p++) { if ((tick & 1) === 0) for (let p = 1; p < IS_SCREW.length; p++) {
if (!IS_SCREW[p]) continue; if (!IS_SCREW[p]) continue;
const list = screwLists[p], dx = SCREW_DX[p], dy = SCREW_DY[p], dj = dy * W + dx, flat = dy === 0; const list = screwLists[p], dx = SCREW_DX[p], dy = SCREW_DY[p], flat = dy === 0;
for (let k = 0; k < list.length; k++) { for (let k = 0; k < list.length; k++) {
const i = list[k]; const i = list[k];
if (part[i] !== p || occ[i]) continue; if (part[i] !== p || occ[i]) continue;
@@ -130,14 +130,14 @@ function machinesTick() {
if (shifted) continue; if (shifted) continue;
} }
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue; if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue;
const dest = dx && dy ? diagonalScrewNext(i, p) : i + dj; const dest = dx && dy ? diagonalScrewNext(i, p) : straightScrewNext(i, p);
if (dest < 0) continue; if (dest < 0) continue;
const nx = dest % W, ny = (dest / W) | 0; const nx = dest % W, ny = (dest / W) | 0;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue; if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = dest; const j = dest;
if (occ[j]) continue; if (occ[j]) continue;
const pj = part[j], screw = IS_SCREW[pj]; const pj = part[j], screw = IS_SCREW[pj];
if (screw && SCREW_DX[pj] === -dx && SCREW_DY[pj] === -dy) continue; if (screw && !screwAccepts(pj, dx, dy)) continue;
const t = mat[j]; const t = mat[j];
if (!roomFor(j)) continue; if (!roomFor(j)) continue;
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue; if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue;
+4 -3
View File
@@ -110,7 +110,7 @@ function drawPreviews() {
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; } if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0); const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
const afford = canAfford(pieceCostNow(P), cells.length); const afford = canAfford(pieceCostNow(P), cells.length);
const planned = (code === BP_SCREW && dIdx >= 4) || isSlopeBelt(code) ? new Map(cells.map(([x, y]) => [y * BW + x, [code, dIdx]])) : null; const planned = code === BP_SCREW || isSlopeBelt(code) ? new Map(cells.map(([x, y]) => [y * BW + x, [code, dIdx]])) : null;
for (const [bx, by] of cells) { for (const [bx, by] of cells) {
const err = canPlaceBlock(bx, by, code), rb = by * BW + bx; const err = canPlaceBlock(bx, by, code), rb = by * BW + bx;
const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, code, v, pieceExtra(P)); const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, code, v, pieceExtra(P));
@@ -180,12 +180,13 @@ function drawPreviews() {
function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1, by = -1, planned = null) { function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1, by = -1, planned = null) {
const ctx = R.ctx; const ctx = R.ctx;
if (code === BP_SCREW && direction >= 4) { if (code === BP_SCREW && (direction >= 4 || (bx >= 0 && straightScrewJoins(bx, by, direction, planned)))) {
const joins = bx >= 0 ? diagonalScrewJoins(bx, by, direction, planned) : 0; const joins = bx >= 0 ? (direction >= 4 ? diagonalScrewJoins(bx, by, direction, planned) : straightScrewJoins(bx, by, direction, planned)) : 0;
const scale = w / BS, step = SET.look === "smooth" ? 0.25 : 1; const scale = w / BS, step = SET.look === "smooth" ? 0.25 : 1;
const b = bx >= 0 ? by * BW + bx : 0, ox = (b % BW) * BS, oy = ((b / BW) | 0) * BS; const b = bx >= 0 ? by * BW + bx : 0, ox = (b % BW) * BS, oy = ((b / BW) | 0) * BS;
ctx.beginPath(); ctx.beginPath();
for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) { for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) {
if (direction < 4 && ((direction & 1) ? xx < 0 || xx >= BS : yy < 0 || yy >= BS)) continue;
const sd = diagonalScrewShape(ox + xx + step / 2, oy + yy + step / 2, b, direction, joins); const sd = diagonalScrewShape(ox + xx + step / 2, oy + yy + step / 2, b, direction, joins);
if (sd <= 0 && (!stroke || sd > -step * 1.5)) ctx.rect(x + xx * scale, y + yy * scale, step * scale, step * scale); if (sd <= 0 && (!stroke || sd > -step * 1.5)) ctx.rect(x + xx * scale, y + yy * scale, step * scale, step * scale);
} }
+66 -61
View File
@@ -3,23 +3,25 @@ const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const fs = require('fs'); const fs = require('fs');
const { run } = load(); const { run } = load();
const { check, done } = suite(); const { check, done } = suite();
run(fs.readFileSync('src/render/diagonal-screws.js', 'utf8'));
run(fs.readFileSync('src/render/world.js', 'utf8'));
function fresh() { flatWorld(run, 256, 192, 176); run('RS=7; nextPowerBuild=0; money=10000; AMB.fill(20); temp.fill(20); hot.fill(0);'); } function fresh() { flatWorld(run, 256, 192, 176); run('RS=7; nextPowerBuild=0; money=10000; AMB.fill(20); temp.fill(20); hot.fill(0);'); }
function line(v, chain = false) { function line(v, chain = false) {
fresh(); fresh();
const [dx, dy] = run(`SCREW_DIRS[${v}]`), bx = dx > 0 ? 16 : 36, by = dy < 0 ? 32 : 12; const [dx, dy] = run(`SCREW_DIRS[${v}]`), bx = dx > 0 ? 16 : 36, by = dy < 0 ? 32 : 12;
run(`for(let k=0;k<10;k++) placeBlock(BP_SCREW,${v},${bx}+k*${dx},${by}+k*${dy},true);`); run(`for(let k=0;k<10;k++) placeBlock(BP_SCREW,${v},${bx}+k*${dx},${by}+k*${dy},true);`);
let tail = bx, head = bx + 9 * dx; let ty = by, tail = bx, head = bx + 9 * dx;
if (chain) { if (chain) {
run(`for(let k=1;k<=3;k++) { placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${bx}-k*${dx},${by},true); placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${head}+k*${dx},${by + 9 * dy},true); }`); run(`for(let k=1;k<=3;k++) { placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${bx}-k*${dx},${by+(dy<0?1:0)},true); placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${head}+k*${dx},${by + 9 * dy+(dy>0?1:0)},true); }`);
tail -= 3 * dx; head += 3 * dx; ty += dy<0?1:0; tail -= 3 * dx; head += 3 * dx;
} }
return { dx, dy, bx, by, tail, head, hy: by + 9 * dy }; return { dx, dy, bx, by, ty, tail, head, hy: by + 9 * dy };
} }
function drain(s, steps, feed = false) { function drain(s, steps, feed = false) {
return run(`(() => { return run(`(() => {
let out=0, leaks=0, added=0; let out=0, leaks=0, added=0;
for(let k=0;k<${steps};k++) { for(let k=0;k<${steps};k++) {
${feed ? `for(let l=0;l<4;l++) { const i=(${s.by}*4+l)*W+${s.tail}*4+${s.dx > 0 ? 0 : 3}; if(mat[i]===EMPTY) {setCell(i,SAND,20,0);added++;} }` : ''} ${feed ? `for(let l=0;l<4;l++) { const i=(${s.ty ?? s.by}*4+l)*W+${s.tail}*4+${s.dx > 0 ? 0 : 3}; if(mat[i]===EMPTY) {setCell(i,SAND,20,0);added++;} }` : ''}
simTick(); simTick();
for(let i=0;i<N;i++) if(mat[i]===SAND && !IS_SCREW[part[i]]) { for(let i=0;i<N;i++) if(mat[i]===SAND && !IS_SCREW[part[i]]) {
const x=i%W; if(${s.dx}>0 ? x>=${(s.head + 1) * 4} : x<${s.head * 4}) out++; else leaks++; const x=i%W; if(${s.dx}>0 ? x>=${(s.head + 1) * 4} : x<${s.head * 4}) out++; else leaks++;
@@ -44,40 +46,61 @@ for (const v of [4, 5, 6, 7]) {
run('rebuildPower();'); run('rebuildPower();');
check(`direction ${v} joins corner-touching power networks`,run('nets.length===2 && nets[1].nt===160')); check(`direction ${v} joins corner-touching power networks`,run('nets.length===2 && nets[1].nt===160'));
const c = line(v, true); const c = line(v, true);
rect(run,c.tail*4,c.by*4,c.tail*4+3,c.by*4+3,run('SAND')); rect(run,c.tail*4,c.ty*4,c.tail*4+3,c.ty*4+3,run('SAND'));
const joined = drain(c,500); const joined = drain(c,500);
check(`direction ${v}: straight to diagonal to straight`,joined.out===16 && joined.leaks===0,JSON.stringify(joined)); check(`direction ${v}: straight to diagonal to straight`,joined.out===16 && joined.leaks===0,JSON.stringify(joined));
} }
// Corner placements are separate mouths, on both axes and in either transfer direction. // Every neighbouring block, including the old edge probes and E, in both directions.
for (const v of [4,5,6,7]) for (const vertical of [false,true]) for (const fromDiagonal of [false,true]) { for (const v of [4,5,6,7]) for (const side of [-1,1]) for (const vertical of [false,true]) {
const slope=v===4||v===6?-1:1, end=side>0?1:0, dy=side*slope;
for (const [ox,oy] of [[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]]) {
const joined=vertical ? ox===side && oy===dy : ox===side && oy===(dy<0?0:dy);
const nx=20+ox, ny=20+oy, sv=vertical?(dy<0?0:2):(side>0?1:3);
const label=`diagonal ${v} end ${end} ${vertical?'vertical':'horizontal'} offset ${ox},${oy}`;
fresh(); fresh();
const [dx,dy]=run(`SCREW_DIRS[${v}]`), sv=vertical?(dy<0?0:2):(dx>0?1:3); run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${sv},${nx},${ny},true);`);
const sx=20+(fromDiagonal?0:-dx), sy=20+(fromDiagonal?0:-dy); const kind=joined?(oy===0?(side>0?2:4):vertical?6:5):0;
const tx=20+(fromDiagonal?dx:0), ty=20+(fromDiagonal?dy:0); check(`${label}: drawing agrees with endJoin`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v}),e=endJoin(20,20,${end});return (e>=0)===${joined} && (j>>${end*3}&7)===${kind} && !!(j&${64<<end})===${!joined};})()`));
const source=fromDiagonal?v:sv, target=fromDiagonal?sv:v; check(`${label}: planned drawing agrees`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v}),sj=straightScrewJoins(${nx},${ny},${sv});removeBlock(${nx},${ny});const plan=new Map([[${ny}*BW+${nx},[BP_SCREW,${sv}]]]);return diagonalScrewJoins(20,20,${v},plan)===j && straightScrewJoins(${nx},${ny},${sv},plan)===sj;})()`));
const label=`corner ${vertical?'vertical':'horizontal'} ${v} ${fromDiagonal?'diagonal to straight':'straight to diagonal'}`; check(`${label}: a complete ghost plan matches placed joins`,run(`(()=>{placeBlock(BP_SCREW,${sv},${nx},${ny},true);const j=diagonalScrewJoins(20,20,${v}),sj=straightScrewJoins(${nx},${ny},${sv});removeBlock(20,20);removeBlock(${nx},${ny});const plan=new Map([[20*BW+20,[BP_SCREW,${v}]],[${ny}*BW+${nx},[BP_SCREW,${sv}]]]);return diagonalScrewJoins(20,20,${v},plan)===j && straightScrewJoins(${nx},${ny},${sv},plan)===sj;})()`));
run(`placeBlock(BP_SCREW,${source},${sx},${sy},true);placeBlock(BP_SCREW,${target},${tx},${ty},true);rebuildPower();`); for (const forward of [true,false]) {
fresh();
const dv=(side>0)===(v<6)?v:(v<6?v+2:v-2), straight=forward?sv:(sv+2)%4, diag=forward?dv:(dv<6?dv+2:dv-2);
const sx=forward?20:nx,sy=forward?20:ny,tx=forward?nx:20,ty=forward?ny:20;
run(`placeBlock(BP_SCREW,${diag},20,20,true);placeBlock(BP_SCREW,${straight},${nx},${ny},true);rebuildPower();`);
rect(run,sx*4,sy*4,sx*4+3,sy*4+3,run('SAND')); rect(run,sx*4,sy*4,sx*4+3,sy*4+3,run('SAND'));
const grains=run(`(()=>{let crossed=0;for(let k=0;k<80;k++){ const crossed=run(`(()=>{let n=0;for(let k=0;k<40;k++){tick+=2;tickStamp++;machinesTick();for(let i=0;i<N;i++)if(mat[i]===SAND){const x=i%W>>2,y=(i/W|0)>>2;if(x===${tx}&&y===${ty})n++;if(x!==${sx}||y!==${sy})setCell(i,EMPTY,20,0);}flyers=[];}return n;})()`);
tick+=2;tickStamp++;machinesTick(); check(`${label}: grains ${forward?'out':'in'} ${joined?'cross':'stay separate'}`,joined?crossed===16:crossed===0,crossed);
for(let i=0;i<N;i++)if(mat[i]===SAND){ {
const x=i%W>>2,y=(i/W|0)>>2; run('for(let i=0;i<N;i++)if(mat[i]===SAND)setCell(i,EMPTY,20,0);');
if(x===${tx} && y===${ty})crossed++; putBar(sx*4,sy*4+1);
if(x!==${sx} || y!==${sy})setCell(i,EMPTY,20,0); const bar=run(`(()=>{const e=ents[0];for(let k=0;k<24;k++){tick+=8;entMark(e,0);entScrewTick(e,entScrewAt(e.ix,e.iy));entMark(e,1);if((e.ix>>2)===${tx}&&(e.iy>>2)===${ty})return true;if(!entScrewAt(e.ix,e.iy))break;}return false;})()`);
}flyers=[]; check(`${label}: bar ${forward?'out':'in'} ${joined?'crosses':'stays separate'}`,bar===joined);
}return crossed;})()`);
check(`${label} does not pass grains`,grains===0,grains);
run('mat.forEach((m,i)=>{if(m===SAND)setCell(i,EMPTY,20,0);});');
for(const lane of [0,1,2]) {
putBar(sx*4,sy*4+lane);
const bar=run(`(()=>{const e=ents[0];let crossed=false;for(let k=0;k<80;k++){
tick+=8;entMark(e,0);entScrewTick(e,SCREW_PARTS[${source}]);entMark(e,1);
if((e.ix>>2)===${tx} && (e.iy>>2)===${ty})crossed=true;
if(!entScrewAt(e.ix,e.iy))break;
}removeEnt(e);return crossed;})()`);
check(`${label} does not pass a bar in lane ${lane}`,!bar);
} }
} }
}
}
fresh();
run('placeBlock(BP_SCREW,1,20,20,true);placeBlock(BP_SCREW,0,21,20,true);rebuildPower();');
rect(run,80,80,83,83,run('SAND'));
check('a horizontal screw still feeds the side of a vertical lift',run(`(()=>{for(let k=0;k<24;k++){tick+=2;tickStamp++;machinesTick();for(let y=80;y<84;y++)for(let x=84;x<88;x++)if(mat[y*W+x]===SAND)return true;}return false;})()`));
// The shared opposite-direction check preserves all eight direction combinations.
check('screw entry rejects exactly the opposite direction',run('SCREW_PARTS.every(p=>SCREW_DIRS.every(([dx,dy])=>screwAccepts(p,dx,dy)===(SCREW_DX[p]!==-dx||SCREW_DY[p]!==-dy)))'));
// Flat belts and filters receive both high same-row and low E discharges on their top face.
for (const v of [4,5,6,7]) for (const side of [-1,1]) for (const code of ['BP_BELT','BP_FILTER']) {
fresh();
const slope=v===4||v===6?-1:1,dy=side*slope,nx=20+side,ny=20+(dy<0?0:dy);
const dv=(side>0)===(v<6)?v:(v<6?v+2:v-2),end=side>0?1:0,kind=dy<0?(end?2:4):5;
run(`placeBlock(BP_SCREW,${dv},20,20,true);placeBlock(${code},${side>0?1:0},${nx},${ny},true);rebuildPower();`);
check(`${code} at diagonal ${v} end ${end} draws the shared join`,run(`(diagonalScrewJoins(20,20,${dv})>>${end*3}&7)===${kind}`));
check(`${code} at diagonal ${v} end ${end} receives every grain lane above its face`,run(`(()=>{for(let l=0;l<4;l++){const i=(80+l)*W+${side>0?83:80};if(diagonalScrewNext(i,SCREW_PARTS[${dv}])!==${ny*4-1}*W+${side>0?84:79})return false;}return true;})()`));
putBar(80,81);
check(`${code} at diagonal ${v} end ${end} receives a bar on its face`,run(`(()=>{const e=ents[0];tick=8;entMark(e,0);entDiagonalScrewTick(e,SCREW_PARTS[${dv}]);entMark(e,1);return e.ix===${nx*4}&&e.iy===${ny*4-2};})()`));
}
// A target's travel direction never changes which geometric corner receives a lane.
fresh();
run('placeBlock(BP_SCREW,1,20,20,true);placeBlock(BP_SCREW,6,21,19,true);');
check('a straight mouth uses the joined corner even when the diagonal points back',run('straightScrewNext(82*W+83,P_SCREW_R)===78*W+84'));
// A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz. // A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz.
const rates = []; const rates = [];
for (const v of [0, 4, 5]) { for (const v of [0, 4, 5]) {
@@ -129,21 +152,20 @@ function watchBars(s, n=600) {
} }
for(const v of [4,5,6,7]) for(const chain of [false,true]) { for(const v of [4,5,6,7]) for(const chain of [false,true]) {
const s=line(v,chain); const s=line(v,chain);
putBar(s.tail*4,s.by*4); putBar(s.tail*4,s.ty*4);
putBar(s.tail*4,s.by*4+2); // a queued bar must wait for the first one putBar(s.tail*4,s.ty*4+2); // a queued bar must wait for the first one
const r=watchBars(s); const r=watchBars(s);
check(`bars travel diagonal ${v}${chain?' and both straight joins':''} without overlap`,r.out===2 && !r.overlap,JSON.stringify(r)); check(`bars travel diagonal ${v}${chain?' and both straight joins':''} without overlap`,r.out===2 && !r.overlap,JSON.stringify(r));
} }
// Vertical straight joins reach the same tails and heads too. // Vertical joins live in E at both ends, then continue along the lift axis.
for (const v of [4,5,6,7]) { for (const v of [4,5,6,7]) {
const s=line(v), vv=s.dy<0?0:2; const s=line(v), vv=s.dy<0?0:2, tailX=s.bx-s.dx, headX=s.head+s.dx;
run(`for(let k=1;k<=3;k++){placeBlock(BP_SCREW,${vv},${s.bx},${s.by}-k*${s.dy},true);placeBlock(BP_SCREW,${vv},${s.head},${s.hy}+k*${s.dy},true);}`); run(`for(let k=1;k<=3;k++){placeBlock(BP_SCREW,${vv},${tailX},${s.by}-k*${s.dy},true);placeBlock(BP_SCREW,${vv},${headX},${s.hy}+k*${s.dy},true);}`);
putBar(s.bx*4,(s.by-3*s.dy)*4+1); putBar(tailX*4,(s.by-3*s.dy)*4+1);
const r=run(`(()=>{let out=false;for(let k=0;k<500;k++){simTick();if(${s.dy}<0?ents[0].iy+2<=${(s.hy-3)*4}:ents[0].iy>=${(s.hy+4)*4})out=true;}return {out,overlap:mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)};})()`); const r=run(`(()=>{let out=false;for(let k=0;k<500;k++){simTick();if(${s.dy}<0?ents[0].iy+2<=${(s.hy-3)*4}:ents[0].iy>=${(s.hy+4)*4})out=true;}return {out,overlap:mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)};})()`);
check(`a bar crosses vertical joins at diagonal ${v}`,r.out && !r.overlap,JSON.stringify(r)); check(`a bar crosses vertical joins at diagonal ${v}`,r.out && !r.overlap,JSON.stringify(r));
// Sand from the vertical intake exits the far vertical head; no outside grains are left behind.
run('for(const e of ents.slice())removeEnt(e);'); run('for(const e of ents.slice())removeEnt(e);');
rect(run,s.bx*4,(s.by-3*s.dy)*4,s.bx*4+3,(s.by-3*s.dy)*4+3,run('SAND')); rect(run,tailX*4,(s.by-3*s.dy)*4,tailX*4+3,(s.by-3*s.dy)*4+3,run('SAND'));
const grain=run(`(()=>{let out=0,leak=0;for(let k=0;k<500;k++){simTick();for(const f of flyers)if(f.m===SAND){if(${s.dy}<0?f.y<${(s.hy-3)*4}:f.y>=${(s.hy+4)*4})out++;else leak++;}flyers=[];}return {out,leak};})()`); const grain=run(`(()=>{let out=0,leak=0;for(let k=0;k<500;k++){simTick();for(const f of flyers)if(f.m===SAND){if(${s.dy}<0?f.y<${(s.hy-3)*4}:f.y>=${(s.hy+4)*4})out++;else leak++;}flyers=[];}return {out,leak};})()`);
check(`sand crosses vertical joins at diagonal ${v}`,grain.out===16 && !grain.leak,JSON.stringify(grain)); check(`sand crosses vertical joins at diagonal ${v}`,grain.out===16 && !grain.leak,JSON.stringify(grain));
} }
@@ -198,27 +220,6 @@ for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.screw,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v); check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.screw,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v);
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`)); check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
} }
// End elbows follow the adjoining straight piece's axis, regardless of flow direction.
run(fs.readFileSync('src/render/diagonal-screws.js', 'utf8'));
run(fs.readFileSync('src/render/world.js', 'utf8'));
for (const v of [4,5,6,7]) for (const end of [-1,1]) for (let dir=0;dir<4;dir++) {
fresh();
const slope=v===4 || v===6?-1:1, nx=20+end, ny=20+end*slope, shift=end>0?3:0;
run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${dir},${nx},${ny},true);`);
check(`corner ${v} end ${end} straight ${dir} keeps its free-end collar`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});return (j>>${shift}&7)===0 && !!(j&${end>0?128:64});})()`));
check(`corner ${v} end ${end} straight ${dir} leaves the free-end geometry unchanged`,run(`(()=>{const b=20*BW+20,j=diagonalScrewJoins(20,20,${v});removeBlock(${nx},${ny});const alone=diagonalScrewJoins(20,20,${v});placeBlock(BP_SCREW,${dir},${nx},${ny},true);return j===alone && [0.5,1.5,2.5,3.5].every(t=>diagonalScrewShape(82+${end}*2,80+t,b,${v},j)===diagonalScrewShape(82+${end}*2,80+t,b,${v},alone));})()`));
check(`corner ${v} end ${end} straight ${dir} planned joins match placed joins`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});removeBlock(${nx},${ny});return diagonalScrewJoins(20,20,${v},new Map([[${ny}*BW+${nx},[BP_SCREW,${dir}]]]))===j;})()`));
}
for (const v of [4,5,6,7]) for (let dir=0;dir<4;dir++) for (const code of ['BP_SCREW','BP_BELT','BP_FILTER']) {
if (code !== 'BP_SCREW' && !(dir & 1)) continue;
fresh();
const [dx,dy]=run(`SCREW_DIRS[${dir}]`);
run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(${code},${dir},${20+dx},${20+dy},true);`);
const end = dx ? dx>0 : (v===4 || v===6 ? dy<0 : dy>0);
check(`diagonal ${v} bends toward ${code} direction ${dir}`,run(`(diagonalScrewJoins(20,20,${v}) >> ${end?3:0} & 7)`)===dir+1);
check(`planned ${code} direction ${dir} bends the diagonal ${v} ghost too`,run(`(()=>{removeBlock(${20+dx},${20+dy});const planned=new Map([[${20+dy}*BW+${20+dx},[${code},${dir}]]]);const j=diagonalScrewJoins(20,20,${v},planned);placeBlock(${code},${dir},${20+dx},${20+dy},true);return (j >> ${end?3:0} & 7)===${dir+1};})()`));
check(`diagonal ${v} meets ${code} direction ${dir} across its centred edge`,run(`(()=>{const b=20*BW+20,j=diagonalScrewJoins(20,20,${v});return [0.5,1.5,2.5,3.5].every(t=>diagonalScrewShape(${dx ? 82+dx*1.99 : '80+t'},${dy ? 82+dy*1.99 : '80+t'},b,${v},j)<=0);})()`));
}
// The drawing can bridge a joint only in unoccupied air; transport parts stay square. // The drawing can bridge a joint only in unoccupied air; transport parts stay square.
for (const v of [4,5,6,7]) { for (const v of [4,5,6,7]) {
flatWorld(run,240,192,176); flatWorld(run,240,192,176);
@@ -233,4 +234,8 @@ for (const v of [4,5,6,7]) {
run(`removeBlock(21,${rising ? 19 : 21});`); run(`removeBlock(21,${rising ? 19 : 21});`);
check(`diagonal ${v} has no stray corner after its end`,run(`diagonalScrewBlock(${x+1},${rising ? y-1 : y+1},false)===-1`)); check(`diagonal ${v} has no stray corner after its end`,run(`diagonalScrewBlock(${x+1},${rising ? y-1 : y+1},false)===-1`));
} }
fresh();
run('placeBlock(BP_SCREW,5,20,20,true);placeBlock(BP_SCREW,1,21,21,true);');
check('shared rule: downhill low end draws a horizontal elbow in E',run('(diagonalScrewJoins(20,20,5)>>3&7)===5'));
check('shared rule: downhill low end sends its last lane into E',run('diagonalScrewNext(83*W+83,P_SCREW_DR)===87*W+84'));
done(); done();
+83 -36
View File
@@ -97,58 +97,105 @@ with sync_playwright() as p:
}''') }''')
check('sidewall stays behind the grain colour in pixels and smooth', check('sidewall stays behind the grain colour in pixels and smooth',
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall) wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
# Edge joins keep closed elbows; corner placements match two isolated free ends. # Shared joins: E elbows bend inside the straight block; other placements keep collars.
pg.set_viewport_size({"width": 2100, "height": 1300}) pg.set_viewport_size({"width": 2100, "height": 1500})
ev("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();") ev("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();")
ev("() => {" + (root / "tools/scenes/t62-joins.js").read_text() + "}") ev("() => {" + (root / "tools/scenes/joins.js").read_text() + "}")
mouths = ev('''() => { mouths = ev('''() => {
SET.zoom=6; SET.lighting="off"; SET.glow="off"; resizeRender(); SET.zoom=4; SET.lighting="off"; SET.glow="off"; resizeRender();
bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera(); bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera();
const now=performance.now.bind(performance),frozen=now(); const now=performance.now.bind(performance),frozen=now();
performance.now=()=>frozen; performance.now=()=>frozen;
try { try {
const out={}; const out={};
const draw=()=>{SM.scale=1;drawWorld();};
for(const look of ["pixel","smooth"]) { for(const look of ["pixel","smooth"]) {
SET.look=look;draw(); SET.look=look;SM.scale=1;drawWorld();
const bad=[]; const bad=[], copper=c=>c[0]>c[1]*1.15 && c[1]>c[2]*1.1;
for(const j of T62_JOINTS) { const sample=(x,y)=>{const p=worldToScreenPx(x,y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
const slope=j.v===4 || j.v===6?-1:1; for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW)) {
const end=j.end*SCREW_DIRS[j.v][0]; const slope=j.v===4 || j.v===6?-1:1, side=j.end?1:-1;
const dx=j.vertical?0:end,dy=j.vertical?end*slope:0; const joins=diagonalScrewJoins(j.bx,j.by,j.v);
if(j.corner)continue; if(!j.joined) {
for(const side of [-1,1])for(const along of [-0.3,0.3]) { if(!(joins & (64<<j.end)) || (joins>>(j.end*3)&7))bad.push(['missing free collar',j]);
const x=j.bx*4+2+dx*(2+along)-dy*side*1.6; // The middle of the free cap, safely inside its diagonal block.
const y=j.by*4+2+dy*(2+along)+dx*side*1.6; const cap=look==='pixel' ? [[1.5,2.5],[2.5,1.5]] : [[1.8,1.8]];
const p=worldToScreenPx(x,y),c=R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data; const colors=cap.map(([x,y])=>sample(j.bx*4+2+side*x,j.by*4+2+side*slope*y));
if(!(c[0]>c[1]*1.15 && c[1]>c[2]*1.1))bad.push([j,x,y,Array.from(c)]); if(!colors.some(copper))bad.push(['free collar colour',j,colors]);
continue;
} }
const ex=(j.kind>=5?j.nx:j.bx)*4+2,ey=(j.kind>=5?j.ny:j.by)*4+2;
const dx=j.vertical?0:side,dy=j.vertical?side*slope:0;
// Both walls on the outgoing axis, beyond the rounded meeting point.
for(const sign of [-1,1]) {
const x=ex+dx*1.5-dy*sign*1.6,y=ey+dy*1.5+dx*sign*1.6,c=sample(x,y);
if(!copper(c))bad.push(['open casing',j,x,y,c]);
}
// Follow both centrelines through the join: the bore must never show sky.
const ax=j.bx*4+2-side*1.5,ay=j.by*4+2-side*slope*1.5;
for(let t=0;t<=1;t+=0.05)for(const leg of [0,1]) {
const x=leg?ex+dx*t*5:ax+(ex-ax)*t,y=leg?ey+dy*t*5:ay+(ey-ay)*t,c=sample(x,y);
if(c[2]>c[0]*1.4 && c[2]>110)bad.push(['background inside tube',j,x,y,c]);
}
if(j.kind>=5) {
// The unused corner of the bent straight block must become background.
const x=j.nx*4+2+(j.vertical?side:-side)*1.8;
const y=j.ny*4+2+(j.vertical?-side*slope:side*slope)*1.8,c=sample(x,y);
if(copper(c) || c[2]<c[0]*1.2)bad.push(['square casing outside elbow',j,x,y,c]);
} }
const sample=(bx,by)=>{
const colors=[];
for(let y=0;y<4;y++)for(let x=0;x<4;x++) {
const p=worldToScreenPx(bx*4+x+0.5,by*4+y+0.5);
colors.push(...R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);
}return colors.join(',');
};
for(const j of T62_JOINTS.filter(j=>j.corner)) {
const [dx,dy]=SCREW_DIRS[j.v],sx=j.bx+j.end*dx,sy=j.by+j.end*dy;
const collar=sample(j.bx,j.by),straight=sample(sx,sy);
const joins=diagonalScrewJoins(j.bx,j.by,j.v),end=j.end*dx>0?1:0;
if(!(joins & (64<<end)) || (joins>>(end*3)&7))bad.push(['missing collar',j]);
removeBlock(sx,sy);draw();
if(sample(j.bx,j.by)!==collar)bad.push(['diagonal differs from isolated end',j]);
placeBlock(BP_SCREW,j.straight,sx,sy,true);
removeBlock(j.bx,j.by);draw();
if(sample(sx,sy)!==straight)bad.push(['straight differs from normal end',j]);
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);draw();
} }
out[look]=bad; out[look]=bad;
}return out; }return out;
} finally {performance.now=now;} } finally {performance.now=now;}
}''') }''')
for look, bad in mouths.items(): for look, bad in mouths.items():
check(f"{look}: edge elbows stay closed and corners show two separate free ends", not bad, bad[:2]) check(f"{look}: shared screw joins have closed elbows, free collars and no bore holes", not bad, bad[:2])
previews = ev('''() => {
const bad=[], now=performance.now.bind(performance),frozen=now();
performance.now=()=>frozen;
try {
for(const look of ['pixel','smooth'])for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW && j.spot===2 && j.end===1 && (j.v===5&&!j.vertical || j.v===4&&j.vertical))) {
SET.look=look;
const side=j.end?1:-1,slope=j.v===4||j.v===6?-1:1;
const ux=2+(j.vertical?side:-side)*1.8,uy=2+(j.vertical?-side*slope:side*slope)*1.8;
const b=j.by*BW+j.bx,nb=j.ny*BW+j.nx;
const plan=new Map([[b,[BP_SCREW,j.v]],[nb,[BP_SCREW,j.straight]]]);
removeBlock(j.bx,j.by);removeBlock(j.nx,j.ny);
const sample=(x,y)=>{const p=worldToScreenPx(j.nx*4+x,j.ny*4+y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data).join(',');};
SM.scale=1;drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
for(const [n,piece] of plan)ghostBlocks.set(n,piece);
drawWorld();
if(sample(ux,uy)!==bg || sample(2,2)===empty)bad.push(['ghost elbow silhouette',look,j]);
ghostBlocks.clear();
const ctx=R.ctx,canvas=document.createElement('canvas');canvas.width=canvas.height=192;
try {
R.ctx=canvas.getContext('2d');R.ctx.fillStyle='#ffffff';
previewBlock(BP_SCREW,64,64,64,64,false,j.straight,j.nx,j.ny,plan);
const alpha=(x,y)=>R.ctx.getImageData(Math.floor(64+x*16),Math.floor(64+y*16),1,1).data[3];
if(alpha(ux,uy)!==0 || alpha(2,2)===0)bad.push(['planned preview elbow silhouette',look,j]);
} finally {R.ctx=ctx;}
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);placeBlock(BP_SCREW,j.straight,j.nx,j.ny,true);
}
} finally {performance.now=now;ghostBlocks.clear();}
return bad;
}''')
check('both looks: ghosts and planned previews leave the unused elbow corner empty', not previews, previews[:2])
tree_tags = ev('''() => {
SET.look='smooth';SM.scale=1;
const now=performance.now.bind(performance),frozen=now();performance.now=()=>frozen;
try {
const j=JOIN_CASES.find(j=>j.v===5 && j.end===1 && !j.vertical && j.spot===2 && j.code===BP_SCREW);
const bx=j.nx+1,by=j.ny;
const sample=()=>{const p=worldToScreenPx(bx*4,by*4);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),4*R.zp,4*R.zp).data).join(',');};
drawWorld();const plain=sample(),bad=[];
for(const m of [WOOD,LEAVES]) {
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=m;
drawWorld();if(sample()!==plain)bad.push(m);
}
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=0;
return bad;
} finally {performance.now=now;}
}''')
check('smooth: trees behind a straight screw cannot become elbow join bits', not tree_tags, tree_tags)
# a browser without hardware drawing gets the pixel look and a note # a browser without hardware drawing gets the pixel look and a note
pg2 = b.new_page(viewport={'width': 800, 'height': 500}) pg2 = b.new_page(viewport={'width': 800, 'height': 500})
pg2.add_init_script('localStorage.clear();') pg2.add_init_script('localStorage.clear();')
+40
View File
@@ -0,0 +1,40 @@
// Shared join grid: eight rows (UR/DR/DL/UL, left/right ends), nine columns.
// Screw H and V, then belt H; each tries the side, above/below, and E.
// Self-contained, paused and empty. Use --zoom 4 --viewport 2100x1500 --no-light.
money = 50000;
for (let y = 40; y < 420; y++) for (let x = 100; x < 560; x++) {
const i = y * W + x;
setCell(i, EMPTY, 20, 0); bgm[i] = 0;
}
for (let x = 100; x < 560; x++) surfY[x] = 420;
window.JOIN_CASES = [];
for (let row = 0; row < 8; row++) for (let col = 0; col < 9; col++) {
const v = 4 + (row >> 1), end = row & 1, side = end ? 1 : -1;
const slope = v === 4 || v === 6 ? -1 : 1, dy = side * slope;
const vertical = col >= 3 && col < 6, belt = col >= 6, spot = col % 3;
const bx = 34 + col * 12, by = 24 + row * 10;
const nx = bx + (spot === 1 ? 0 : side), ny = by + (spot === 0 ? 0 : dy);
const code = belt ? BP_BELT : BP_SCREW;
const straight = belt ? (side > 0 ? 1 : 0) : vertical ? (dy < 0 ? 0 : 2) : (side > 0 ? 1 : 3);
const diagonalCode = belt ? (slope < 0 ? BP_BELT_UP : BP_BELT_DOWN) : BP_SCREW;
const diagonalV = belt ? (SCREW_DIRS[v][0] > 0 ? 1 : 0) : v;
for (let k = 0; k < 2; k++) placeBlock(diagonalCode, diagonalV, bx - k * side, by - k * dy, true);
for (let k = 0; k < 2; k++) placeBlock(code, straight, nx + (vertical ? 0 : k * side * (spot === 1 ? -1 : 1)), ny + (vertical ? k * dy * (spot === 0 ? -1 : 1) : 0), true);
const joined = endJoin(bx, by, end) >= 0;
const kind = joined ? (ny === by ? (end ? 2 : 4) : vertical ? 6 : 5) : 0;
JOIN_CASES.push({v, end, bx, by, nx, ny, vertical, code, straight, joined, kind, spot});
}
updateRoof();
window.CAM = [326, 232];
// Labels are part of the scene so screenshots and browser checks use the same layout.
drawBot = () => {
const ctx = R.ctx;
ctx.save(); ctx.font = '12px monospace'; ctx.textBaseline = 'bottom';
for (const j of JOIN_CASES) {
const p = worldToScreenPx(j.bx * BS - 8, j.by * BS - 14);
ctx.fillStyle = '#17222c';
const name = ['UR', 'DR', 'DL', 'UL'][j.v - 4], axis = j.code === BP_BELT ? 'belt' : j.vertical ? 'V' : 'H';
ctx.fillText(`${name} ${j.end ? 'R' : 'L'} ${axis} ${['side', 'up/down', 'E'][j.spot]}: ${j.joined ? 'JOIN' : 'free'}`, p.x, p.y);
}
ctx.restore();
};
-27
View File
@@ -1,27 +0,0 @@
// Self-contained grid. Rows: UR, DR, DL, UL. Each run shows both ends.
// Columns: horizontal edge, horizontal corner, vertical edge, vertical corner.
// Corner columns stay unconnected, with two separate ends.
// Repeat those four with the straight travel reversed. Camera fits --zoom 6 --viewport 2100x1300.
// Run after clear-sky.js or by itself. Empty, paused screws expose the whole casing.
money = 50000;
for (let y = 64; y < 280; y++) for (let x = 112; x < 456; x++) {
const i = y * W + x;
setCell(i, EMPTY, 20, 0); bgm[i] = 0;
}
for (let x = 112; x < 456; x++) surfY[x] = 280;
window.T62_JOINTS = [];
for (let row = 0; row < 4; row++) for (let col = 0; col < 8; col++) {
const v = 4 + row, [dx, dy] = SCREW_DIRS[v], vertical = !!(col & 2), corner = !!(col & 1);
const x = 33 + col * 10 + (dx < 0 ? 4 : 0), y = 23 + row * 12 + (dy < 0 ? 4 : 0);
const sv = vertical ? (dy < 0 ? 0 : 2) : (dx > 0 ? 1 : 3), straight = col < 4 ? sv : (sv + 2) % 4;
for (let k = 0; k < 3; k++) placeBlock(BP_SCREW, v, x + k * dx, y + k * dy, true);
for (const end of [-1, 1]) {
const ex = x + (end > 0 ? 2 * dx : 0), ey = y + (end > 0 ? 2 * dy : 0);
const nx = ex + (vertical && !corner ? 0 : end * dx), ny = ey + (!vertical && !corner ? 0 : end * dy);
for (let k = 0; k < 2; k++) placeBlock(BP_SCREW, straight, nx + (vertical ? 0 : k * end * dx), ny + (vertical ? k * end * dy : 0), true);
T62_JOINTS.push({v, end, vertical, corner, straight, bx:ex, by:ey});
}
}
updateRoof();
window.CAM = [284, 168];
drawBot = () => {};