Shared join module for transport pieces; belt drawing uses it (t67 phase 1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
-5
@@ -486,11 +486,16 @@ above, including:
|
|||||||
on warns first that the save will be marked, and the mark stays with the world; any future
|
on warns first that the save will be marked, and the mark stays with the world; any future
|
||||||
achievements won't unlock in a marked world. Besides money, items and upgrades it may have
|
achievements won't unlock in a marked world. Besides money, items and upgrades it may have
|
||||||
game speed and other things just for fun.
|
game speed and other things just for fun.
|
||||||
- **Screw conveyors only join a diagonal on the same row (t62, 2026-10-27).** A straight screw
|
- **One join rule for every transport piece (2026-10-27, replaces the t62 same-row rule).** The
|
||||||
joins a diagonal when it sits right beside the diagonal's end block, and that join is drawn
|
owner wants all pieces to place and join the same way, so players know what to expect when
|
||||||
as an elbow. When the diagonal instead starts one block lower or higher, so the two only touch
|
building. At a diagonal piece's end, call E the block diagonally past its last block (the
|
||||||
corner to corner, they stay two separate ends and nothing passes between them. The owner tried
|
block its end points into). Another diagonal of the same slope continues in E. A horizontal
|
||||||
a corner join and found it looked odd, and building it that way is unusual anyway.
|
piece (flat belt, flat filter, horizontal screw) joins on the same row at the high end, and
|
||||||
|
in E, one block lower, at the low end: the rule sloped belts already had. A vertical screw
|
||||||
|
joins in E at either end. Straight pieces join the next block along their axis. Any other
|
||||||
|
placement is two separate ends and nothing passes between them. The owner found the old
|
||||||
|
screw rule, which needed the horizontal placed over the diagonal's last block at the low
|
||||||
|
end, inconsistent with every other piece.
|
||||||
|
|
||||||
## Ideas not built yet
|
## Ideas not built yet
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -118,6 +118,13 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
- kind `mold`.
|
- kind `mold`.
|
||||||
`hidden` pieces are not in the menu. The menu's tabs and order are `BUILD_CATS` in
|
`hidden` pieces are not in the menu. The menu's tabs and order are `BUILD_CATS` in
|
||||||
`src/ui/build-menu.js`.
|
`src/ui/build-menu.js`.
|
||||||
|
- **Transport geometry:** `joins.js` owns geometric ends and joins for belts, filters and screws.
|
||||||
|
`pieceEnds(code, v)` returns precomputed left/right ends (vertical screws: up/down).
|
||||||
|
`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.
|
||||||
|
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
|
||||||
|
this module; screw drawing and movement still use their existing rules.
|
||||||
- **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`);
|
||||||
@@ -265,6 +272,11 @@ 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 conveyors (machinesTick).
|
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
|
||||||
- functions: engineIntake, fanBlow, machinesTick
|
- functions: engineIntake, fanBlow, machinesTick
|
||||||
|
|
||||||
|
### src/sim/joins.js (71 lines)
|
||||||
|
Geometric transport ends. Tables are built once; querying a world or plan allocates nothing. Ends are left/right, except vertical screws (up/down). Travel direction is irrelevant.
|
||||||
|
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins
|
||||||
|
- variables: JOIN_ENDS
|
||||||
|
|
||||||
### src/sim/conveyors.js (198 lines)
|
### src/sim/conveyors.js (198 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, diagonalScrewNext
|
||||||
@@ -362,7 +374,7 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
|
|||||||
Propagated coloured light from glowing and burning cells.
|
Propagated coloured light from glowing and burning cells.
|
||||||
- functions: builtForLight, looseForLight, computeLight
|
- functions: builtForLight, looseForLight, computeLight
|
||||||
|
|
||||||
### src/render/slope-belts.js (176 lines)
|
### src/render/slope-belts.js (157 lines)
|
||||||
Drawing only: the face stays on its simulation cells; the band hangs below it.
|
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
|
||||||
@@ -372,7 +384,7 @@ Screw casing geometry shared by both looks, ghosts and placement previews. smoot
|
|||||||
- functions: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
- functions: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
||||||
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
|
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
|
||||||
|
|
||||||
### src/render/world.js (402 lines)
|
### src/render/world.js (403 lines)
|
||||||
Scale colour without changing the smooth-look class.
|
Scale colour without changing the smooth-look class.
|
||||||
- functions: fogColor, drawWorld
|
- functions: fogColor, drawWorld
|
||||||
|
|
||||||
@@ -530,6 +542,7 @@ The frame loop and boot.
|
|||||||
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
||||||
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||||
|
- `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables.
|
||||||
- `test/t_light.js`: Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
- `test/t_light.js`: Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
||||||
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
||||||
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ 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 1, shared join module (Claude Code + Codex)
|
||||||
|
|
||||||
|
- Owner decision (DESIGN.md, replaces the t62 same-row rule): one join rule for every transport
|
||||||
|
piece. New DOM-free `src/sim/joins.js`: `pieceEnds`, `endJoin`, `pieceJoins`; rule tables are
|
||||||
|
compiled once and joins must be mutual. Belt drawing now uses it: `slopeBeltConnections`,
|
||||||
|
`flatBeltCaps` (now also for ghosts), and the pixel filter frame (`beltLike(b, end)`). Sloped
|
||||||
|
belt drag previews now pass a planned map.
|
||||||
|
- Deliberate drawing changes where the old belt rules disagreed: flat belt caps no longer
|
||||||
|
treat any same-row diagonal screw as joined, and filter frames open only at a real join.
|
||||||
|
- Files: `src/sim/joins.js`, `src/manifest.json`, `src/render/slope-belts.js`,
|
||||||
|
`src/render/setup.js`, `src/render/world.js`, `src/ui/previews.js`, `test/t_joins.js`,
|
||||||
|
`test/t_slope_belt_art.js`, `docs/PROJECT_MAP.md`, `docs/DESIGN.md`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed; t_joins checks 4,232 placements,
|
||||||
|
symmetry, planned maps, and that belt sand flow agrees with the rule. Sim output unchanged.
|
||||||
|
- Open: screws still use their old rules until phase 2.
|
||||||
|
|
||||||
## 2026-10-27: t62 corner placements stay separate (Claude Code + Codex)
|
## 2026-10-27: t62 corner placements stay separate (Claude Code + Codex)
|
||||||
|
|
||||||
- Owner tried the corner join and found the jog odd; decision (recorded in DESIGN.md): only
|
- Owner tried the corner join and found the jog odd; decision (recorded in DESIGN.md): only
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"sim/racks.js",
|
"sim/racks.js",
|
||||||
"sim/pipes.js",
|
"sim/pipes.js",
|
||||||
"sim/machine-tick.js",
|
"sim/machine-tick.js",
|
||||||
|
"sim/joins.js",
|
||||||
"sim/conveyors.js",
|
"sim/conveyors.js",
|
||||||
"sim/slope-conveyors.js",
|
"sim/slope-conveyors.js",
|
||||||
"sim/blocks.js",
|
"sim/blocks.js",
|
||||||
|
|||||||
+2
-2
@@ -116,8 +116,8 @@ function thinPipeColor(x, y, anim) {
|
|||||||
}
|
}
|
||||||
PC[0] = r; PC[1] = g; PC[2] = bl;
|
PC[0] = r; PC[1] = g; PC[2] = bl;
|
||||||
}
|
}
|
||||||
// a block that is a belt or a filter: they join up into one belt
|
// A filter frame opens only where this end has a geometric partner.
|
||||||
function beltLike(b) { if (b < 0 || b >= NB) return false; const c = blockP[b]; return isBelt(c) || isFilter(c); }
|
function beltLike(b, end) { return endJoin(b % BW, (b / BW) | 0, end) >= 0; }
|
||||||
// standing trees: the trunk shows its dark bark, so it reads apart from the pale cut wood it drops
|
// standing trees: the trunk shows its dark bark, so it reads apart from the pale cut wood it drops
|
||||||
const TREE_R = new Uint8Array(NMAT), TREE_G = new Uint8Array(NMAT), TREE_B = new Uint8Array(NMAT);
|
const TREE_R = new Uint8Array(NMAT), TREE_G = new Uint8Array(NMAT), TREE_B = new Uint8Array(NMAT);
|
||||||
for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
|
for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
|
||||||
|
|||||||
@@ -2,36 +2,17 @@
|
|||||||
let slopeBeltJoins = new Uint8Array(0), slopeBeltHalo = new Uint8Array(0), beltCaps = new Uint8Array(0);
|
let slopeBeltJoins = new Uint8Array(0), slopeBeltHalo = new Uint8Array(0), beltCaps = new Uint8Array(0);
|
||||||
let slopeAcross = 0, slopeAlong = 0, slopeDistance = 0;
|
let slopeAcross = 0, slopeAlong = 0, slopeDistance = 0;
|
||||||
function slopeBeltConnections(bx, by, code, planned = null) {
|
function slopeBeltConnections(bx, by, code, planned = null) {
|
||||||
const grade = beltGrade(code);
|
|
||||||
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 * grade;
|
const join = endJoin(bx, by, end, planned);
|
||||||
const nb = ny * BW + nx, next = planned && planned.get(nb);
|
if (join < 0) continue;
|
||||||
if (nx >= 0 && nx < BW && ny >= 0 && ny < BH && beltGrade(next ? next[0] : blockP[nb]) === grade) {
|
const nb = join >> 1, next = planned && planned.get(nb);
|
||||||
joins |= 4 << end; continue;
|
joins |= (isSlopeBelt(next ? next[0] : blockP[nb]) ? 4 : 1) << end;
|
||||||
}
|
|
||||||
const fy = by + (end ? grade > 0 : grade < 0), fb = fy * BW + nx;
|
|
||||||
if (nx < 0 || nx >= BW || fy < 0 || fy >= BH) continue;
|
|
||||||
const flat = planned && planned.get(fb), c = flat ? flat[0] : blockP[fb], v = flat ? flat[1] : blockV[fb];
|
|
||||||
if (c === BP_BELT || c === BP_FILTER || (c === BP_SCREW && (v === 1 || v === 3))) joins |= 1 << end;
|
|
||||||
}
|
}
|
||||||
return joins;
|
return joins;
|
||||||
}
|
}
|
||||||
function flatBeltCaps(b) {
|
function flatBeltCaps(b, planned = null) {
|
||||||
const bx = b % BW, by = (b / BW) | 0;
|
return 3 ^ pieceJoins(b % BW, (b / BW) | 0, planned);
|
||||||
let caps = 0;
|
|
||||||
for (let end = 0; end < 2; end++) {
|
|
||||||
const x = bx + (end ? 1 : -1);
|
|
||||||
let joined = false;
|
|
||||||
if (x >= 0 && x < BW) for (let dy = -1; dy <= 0; dy++) {
|
|
||||||
if (by + dy < 0) continue;
|
|
||||||
const n = (by + dy) * BW + x, c = blockP[n], g = beltGrade(c);
|
|
||||||
if ((!dy && (c === BP_BELT || c === BP_FILTER || (c === BP_SCREW && (blockV[n] === 1 || blockV[n] === 3 || blockV[n] >= 4)))) ||
|
|
||||||
(g && dy === -(end ? g < 0 : g > 0))) joined = true;
|
|
||||||
}
|
|
||||||
if (!joined) caps |= 1 << end;
|
|
||||||
}
|
|
||||||
return caps;
|
|
||||||
}
|
}
|
||||||
function prepareSlopeBelts(x0, y0, x1, y1) {
|
function prepareSlopeBelts(x0, y0, x1, y1) {
|
||||||
if (slopeBeltJoins.length !== NB) { slopeBeltJoins = new Uint8Array(NB); slopeBeltHalo = new Uint8Array(NB); beltCaps = new Uint8Array(NB); }
|
if (slopeBeltJoins.length !== NB) { slopeBeltJoins = new Uint8Array(NB); slopeBeltHalo = new Uint8Array(NB); beltCaps = new Uint8Array(NB); }
|
||||||
|
|||||||
+3
-2
@@ -201,7 +201,7 @@ function drawWorld() {
|
|||||||
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
|
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
|
||||||
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
|
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
|
||||||
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
|
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
|
||||||
} else if ((lx === 0 && !beltLike(fb - 1)) || (lx === 3 && !beltLike(fb + 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
|
} else if ((lx === 0 && !beltLike(fb, 0)) || (lx === 3 && !beltLike(fb, 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
|
||||||
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
|
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
|
||||||
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
|
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
|
||||||
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
|
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
|
||||||
@@ -341,6 +341,7 @@ function drawWorld() {
|
|||||||
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;
|
||||||
const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : 0;
|
const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : 0;
|
||||||
|
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) {
|
||||||
@@ -365,7 +366,7 @@ function drawWorld() {
|
|||||||
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
|
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
|
||||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||||
const k = sy * bw + sx, c = px[k];
|
const k = sy * bw + sx, c = px[k];
|
||||||
const edge = slope || diagonal ? sd > -0.8 : dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1;
|
const edge = slope || diagonal ? sd > -0.8 : (dx === 0 && (caps & 1)) || dy === 0 || (dx === BS - 1 && (caps & 2)) || dy === BS - 1;
|
||||||
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
|
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
|
||||||
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
|
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
|
||||||
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
|
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Geometric transport ends. Tables are built once; querying a world or plan allocates nothing.
|
||||||
|
// Ends are left/right, except vertical screws (up/down). Travel direction is irrelevant.
|
||||||
|
const JOIN_ENDS = [[], ...[
|
||||||
|
['screw-horizontal', 1, 0], ['screw-vertical', 0, 1],
|
||||||
|
['screw-diagonal', 1, -1], ['screw-diagonal', 1, 1],
|
||||||
|
['belt-flat', 1, 0], ['belt-sloped', 1, -1], ['belt-sloped', 1, 1]
|
||||||
|
].map(([kind, dx, dy]) => [-1, 1].map(s => ({
|
||||||
|
dx: s * dx, dy: s * dy, diagonal: !!(dx && dy),
|
||||||
|
high: !!(dx && dy && s * dy < 0), low: !!(dx && dy && s * dy > 0), kind,
|
||||||
|
targets: []
|
||||||
|
})))];
|
||||||
|
function pieceEnds(code, v) {
|
||||||
|
if (code === BP_SCREW) {
|
||||||
|
if (v < 0 || v > 7) return JOIN_ENDS[0];
|
||||||
|
return JOIN_ENDS[v < 4 ? (v & 1 ? 1 : 2) : (SCREW_DIRS[v][0] * SCREW_DIRS[v][1] < 0 ? 3 : 4)];
|
||||||
|
}
|
||||||
|
if (code === BP_BELT || code === BP_FILTER) return JOIN_ENDS[5];
|
||||||
|
return isSlopeBelt(code) ? JOIN_ENDS[beltGrade(code) < 0 ? 6 : 7] : JOIN_ENDS[0];
|
||||||
|
}
|
||||||
|
// Compile the diagonal-end rule and its inverse into bounded neighbour probes.
|
||||||
|
for (const ends of JOIN_ENDS) for (const end of ends) {
|
||||||
|
for (const others of JOIN_ENDS) for (let n = 0; n < others.length; n++) {
|
||||||
|
const other = others[n], screw = end.kind.startsWith('screw'), otherScrew = other.kind.startsWith('screw');
|
||||||
|
if (screw !== otherScrew && (end.kind === 'screw-vertical' || other.kind === 'screw-vertical' ||
|
||||||
|
(end.diagonal && other.diagonal))) continue;
|
||||||
|
let dx, dy;
|
||||||
|
if (end.diagonal && other.diagonal) {
|
||||||
|
if (screw !== otherScrew || end.dx !== -other.dx || end.dy !== -other.dy) continue;
|
||||||
|
dx = end.dx; dy = end.dy;
|
||||||
|
} else if (end.diagonal) {
|
||||||
|
if (other.dx ? other.dx !== -end.dx : other.dy !== -end.dy) continue;
|
||||||
|
dx = end.dx; dy = other.dx && end.high ? 0 : end.dy;
|
||||||
|
} else if (other.diagonal) {
|
||||||
|
if (end.dx ? end.dx !== -other.dx : end.dy !== -other.dy) continue;
|
||||||
|
dx = -other.dx; dy = end.dx && other.high ? 0 : -other.dy;
|
||||||
|
} else {
|
||||||
|
if (end.dx !== -other.dx || end.dy !== -other.dy) continue;
|
||||||
|
dx = end.dx; dy = end.dy;
|
||||||
|
}
|
||||||
|
end.targets.push({ dx, dy, ends: others, end: n });
|
||||||
|
}
|
||||||
|
// Prefer a continuing diagonal run to an elbow when both are present.
|
||||||
|
end.targets.sort((a, b) => Number(b.ends[b.end].diagonal) - Number(a.ends[a.end].diagonal));
|
||||||
|
}
|
||||||
|
// Result: neighbour block index * 2 + its end index; -1 means an open end.
|
||||||
|
function joinCandidate(bx, by, endIndex, planned) {
|
||||||
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return -1;
|
||||||
|
const b = by * BW + bx, plan = planned && planned.get(b);
|
||||||
|
const ends = pieceEnds(plan ? plan[0] : blockP[b], plan ? plan[1] : blockV[b]);
|
||||||
|
const end = ends[endIndex];
|
||||||
|
if (!end) return -1;
|
||||||
|
for (let k = 0; k < end.targets.length; k++) {
|
||||||
|
const target = end.targets[k];
|
||||||
|
const x = bx + target.dx, y = by + target.dy;
|
||||||
|
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
|
||||||
|
const nb = y * BW + x, next = planned && planned.get(nb);
|
||||||
|
if (pieceEnds(next ? next[0] : blockP[nb], next ? next[1] : blockV[nb]) === target.ends)
|
||||||
|
return nb * 2 + target.end;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
function endJoin(bx, by, endIndex, planned = null) {
|
||||||
|
const join = joinCandidate(bx, by, endIndex, planned);
|
||||||
|
if (join < 0) return -1;
|
||||||
|
const nb = join >> 1;
|
||||||
|
// Competing neighbours cannot claim an end that chose a different partner.
|
||||||
|
return joinCandidate(nb % BW, (nb / BW) | 0, join & 1, planned) === (by * BW + bx) * 2 + endIndex ? join : -1;
|
||||||
|
}
|
||||||
|
function pieceJoins(bx, by, planned = null) {
|
||||||
|
return (endJoin(bx, by, 0, planned) >= 0 ? 1 : 0) | (endJoin(bx, by, 1, planned) >= 0 ? 2 : 0);
|
||||||
|
}
|
||||||
+1
-1
@@ -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 ? new Map(cells.map(([x, y]) => [y * BW + x, [BP_SCREW, dIdx]])) : null;
|
const planned = (code === BP_SCREW && dIdx >= 4) || 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));
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
// The oracle states the geometric rule independently of the precomputed neighbour tables.
|
||||||
|
const { load, suite, flatWorld } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
flatWorld(run, 96, 96, 92);
|
||||||
|
run(`
|
||||||
|
var joinCases = [];
|
||||||
|
for (let v = 0; v < 8; v++) joinCases.push([BP_SCREW, v]);
|
||||||
|
for (const c of [BP_BELT, BP_FILTER, BP_BELT_UP, BP_BELT_DOWN, BP_FILTER_UP, BP_FILTER_DOWN])
|
||||||
|
for (const v of c === BP_FILTER || isFilter(c) ? [0, 128, 255] : [0, 1]) joinCases.push([c, v]);
|
||||||
|
`);
|
||||||
|
const cases = run('joinCases');
|
||||||
|
const screw = run('BP_SCREW');
|
||||||
|
const dirs = run('SCREW_DIRS');
|
||||||
|
function describe([code, v]) {
|
||||||
|
if (code === screw) {
|
||||||
|
const [x, y] = dirs[v];
|
||||||
|
return { family: 'screw', diagonal: !!(x && y), x: x ? 1 : 0, y: x ? (x * y) : 1 };
|
||||||
|
}
|
||||||
|
const grade = run(`beltGrade(${code})`);
|
||||||
|
return { family: 'belt', diagonal: !!grade, x: 1, y: grade };
|
||||||
|
}
|
||||||
|
function oracle(a, b, dx, dy, ae, be) {
|
||||||
|
const ax = a.x * (ae ? 1 : -1), ay = a.y * (ae ? 1 : -1);
|
||||||
|
const bx = b.x * (be ? 1 : -1), by = b.y * (be ? 1 : -1);
|
||||||
|
if (a.family !== b.family && ((!a.x || !b.x) || (a.diagonal && b.diagonal))) return false;
|
||||||
|
if (a.diagonal && b.diagonal)
|
||||||
|
return a.family === b.family && ax === -bx && ay === -by && dx === ax && dy === ay;
|
||||||
|
if (a.diagonal)
|
||||||
|
return (b.x ? bx === -ax : by === -ay) && dx === ax && dy === (b.x && ay < 0 ? 0 : ay);
|
||||||
|
if (b.diagonal) return oracle(b, a, -dx, -dy, be, ae);
|
||||||
|
return ax === -bx && ay === -by && dx === ax && dy === ay;
|
||||||
|
}
|
||||||
|
let tableErrors = [], symmetryErrors = [], pairCount = 0;
|
||||||
|
for (const ac of cases) for (const bc of cases) {
|
||||||
|
const a = describe(ac), b = describe(bc);
|
||||||
|
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
||||||
|
if (!dx && !dy) continue;
|
||||||
|
const result = run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
const a = 10 * BW + 10, b = (10 + ${dy}) * BW + 10 + ${dx};
|
||||||
|
blockP[a] = ${ac[0]}; blockV[a] = ${ac[1]}; blockP[b] = ${bc[0]}; blockV[b] = ${bc[1]};
|
||||||
|
return [a,b,endJoin(10,10,0),endJoin(10,10,1),endJoin(10+${dx},10+${dy},0),endJoin(10+${dx},10+${dy},1),pieceJoins(10,10)];
|
||||||
|
})()`);
|
||||||
|
let mask = 0;
|
||||||
|
for (let ae = 0; ae < 2; ae++) {
|
||||||
|
let expected = -1;
|
||||||
|
for (let be = 0; be < 2; be++) if (oracle(a, b, dx, dy, ae, be)) expected = result[1] * 2 + be;
|
||||||
|
if (expected >= 0) mask |= 1 << ae;
|
||||||
|
if (result[2 + ae] !== expected) tableErrors.push([ac,bc,dx,dy,ae,expected,result[2+ae]]);
|
||||||
|
for (let be = 0; be < 2; be++)
|
||||||
|
if ((result[2+ae] === result[1]*2+be) !== (result[4+be] === result[0]*2+ae)) symmetryErrors.push([ac,bc,dx,dy,ae,be]);
|
||||||
|
}
|
||||||
|
if (mask !== result[6]) tableErrors.push(['mask', ac,bc,dx,dy]);
|
||||||
|
pairCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check('every kind and travel direction matches the rule throughout the 3 by 3 neighbourhood', !tableErrors.length, `${pairCount} pairs; ${JSON.stringify(tableErrors.slice(0,3))}`);
|
||||||
|
check('every joined pair names the reciprocal end', !symmetryErrors.length, JSON.stringify(symmetryErrors.slice(0,3)));
|
||||||
|
function pair(label, a, b, dx, dy, expected) {
|
||||||
|
check(label, run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
blockP[10*BW+10]=${a[0]}; blockV[10*BW+10]=${a[1]};
|
||||||
|
blockP[(10+${dy})*BW+10+${dx}]=${b[0]}; blockV[(10+${dy})*BW+10+${dx}]=${b[1]};
|
||||||
|
return pieceJoins(10,10)!==0;
|
||||||
|
})()`) === expected);
|
||||||
|
}
|
||||||
|
const ids = run('[BP_SCREW,BP_BELT,BP_FILTER,BP_BELT_UP,BP_BELT_DOWN]');
|
||||||
|
pair('downhill low end meets a horizontal in E', [screw,5], [ids[1],0], 1,1,true);
|
||||||
|
pair('downhill low end leaves a same-row horizontal separate', [screw,5], [ids[1],0], 1,0,false);
|
||||||
|
pair('uphill high end meets a same-row horizontal', [screw,4], [ids[2],128], 1,0,true);
|
||||||
|
pair('uphill high end leaves a horizontal in E separate', [screw,4], [ids[2],128], 1,-1,false);
|
||||||
|
for (const side of [-1,1]) {
|
||||||
|
pair(`vertical screw meets diagonal in E at end ${side}`, [screw,4], [screw,0], side,-side,true);
|
||||||
|
pair(`vertical screw directly above or below stays separate at end ${side}`, [screw,4], [screw,0], 0,side,false);
|
||||||
|
}
|
||||||
|
pair('opposite travel direction continues the same diagonal slope', [screw,4], [screw,6], 1,-1,true);
|
||||||
|
pair('different diagonal slopes stay separate', [screw,4], [screw,5], 1,-1,false);
|
||||||
|
pair('different diagonal families stay separate', [screw,4], [ids[3],0], 1,-1,false);
|
||||||
|
pair('horizontal screw meets a sloped belt', [ids[4],0], [screw,3], 1,1,true);
|
||||||
|
pair('vertical screw never meets a flat belt', [ids[1],0], [screw,0], 1,0,false);
|
||||||
|
check('planned source, neighbour, direction and empty entries override the world', run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
const a=10*BW+10,b=a+1, p=new Map([[a,[BP_BELT,0]],[b,[BP_SCREW,3,'extra']]]);
|
||||||
|
blockP[a]=BP_WALL; blockP[b]=BP_SCREW; blockV[b]=0;
|
||||||
|
if(endJoin(10,10,1,p)!==b*2 || endJoin(11,10,0,p)!==a*2+1) return false;
|
||||||
|
p.set(b,[0,0]);
|
||||||
|
return endJoin(10,10,1,p)===-1 && endJoin(10,10,1)===-1;
|
||||||
|
})()`));
|
||||||
|
check('non-transport pieces, invalid ends and world edges are open without row wrapping', run(`(() => {
|
||||||
|
blockP.fill(BP_BELT); blockV.fill(0);
|
||||||
|
return pieceEnds(BP_WALL,0).length===0 && endJoin(-1,0,0)===-1 && endJoin(BW,0,0)===-1 &&
|
||||||
|
endJoin(0,-1,0)===-1 && endJoin(0,BH,0)===-1 && endJoin(0,0,0)===-1 &&
|
||||||
|
endJoin(BW-1,BH-1,1)===-1 && endJoin(1,1,2)===-1;
|
||||||
|
})()`));
|
||||||
|
// Drive the existing column carrier across a single boundary in both directions. No gravity,
|
||||||
|
// power or cleat timing can disguise a wrong face height as a successful handoff.
|
||||||
|
for (const grade of [-1,1]) for (const side of [-1,1]) for (const joined of [true,false]) for (const reverse of [false,true]) {
|
||||||
|
flatWorld(run,96,96,92);
|
||||||
|
const result = run(`(() => {
|
||||||
|
const c=${grade<0?'BP_BELT_UP':'BP_BELT_DOWN'}, side=${side}, low=side*${grade}>0;
|
||||||
|
const fy=10+(${joined} ? Number(low) : (low ? 0 : -1));
|
||||||
|
placeBlock(c,1,10,10,true); placeBlock(BP_BELT,1,10+side,fy,true);
|
||||||
|
const sx=side>0?43:40, fx=sx+side, sy=beltFaceY(c,10,10,sx), ty=fy*BS;
|
||||||
|
const x=${reverse}?fx:sx, y=${reverse}?ty:sy, d=${reverse}?-side:side;
|
||||||
|
const dest=(${reverse}?sy-1:ty-1)*W+(${reverse}?sx:fx);
|
||||||
|
setCell((y-1)*W+x,SAND,20,0); tickStamp++;
|
||||||
|
const step=conveyorStep(x,y,d);
|
||||||
|
carryColumn((y-1)*W+x,d,null);
|
||||||
|
return [endJoin(10,10,side>0?1:0)>=0,mat[dest]===SAND,step];
|
||||||
|
})()`);
|
||||||
|
check(`grade ${grade} ${side<0?'left':'right'} ${joined?'joined':'separate'} ${reverse?'flat to slope':'slope to flat'} sand handoff matches geometry`,result[0]===joined && result[1]===joined,JSON.stringify(result));
|
||||||
|
}
|
||||||
|
// Render adapters keep their established bit layouts and honour plans without world writes.
|
||||||
|
const fs = require('fs');
|
||||||
|
run(fs.readFileSync(require('path').join(__dirname, '../src/render/slope-belts.js'), 'utf8'));
|
||||||
|
const setup = fs.readFileSync(require('path').join(__dirname, '../src/render/setup.js'), 'utf8');
|
||||||
|
run(setup.slice(setup.indexOf('function beltLike('), setup.indexOf('\n', setup.indexOf('function beltLike('))));
|
||||||
|
check('filter frames follow slope ends and horizontal screw joins', run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
const b=10*BW+10;
|
||||||
|
blockP[b]=BP_FILTER; blockP[b+1]=BP_BELT_UP;
|
||||||
|
if(beltLike(b,1)) return false;
|
||||||
|
blockP[b+1]=BP_BELT_DOWN;
|
||||||
|
if(!beltLike(b,1)) return false;
|
||||||
|
blockP[b+1]=BP_SCREW; blockV[b+1]=1;
|
||||||
|
return beltLike(b,1);
|
||||||
|
})()`));
|
||||||
|
check('planned flat caps and slope flat/run bits agree with the shared ends', run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
const b=10*BW+10,p=new Map([[b,[BP_BELT_DOWN,0]],[b-1,[BP_FILTER,128]],[b+BW+1,[BP_FILTER_DOWN,128]]]);
|
||||||
|
const before=JSON.stringify([Array.from(blockP),Array.from(blockV)]);
|
||||||
|
if(slopeBeltConnections(10,10,BP_BELT_DOWN,p)!==9 || flatBeltCaps(b-1,p)!==1) return false;
|
||||||
|
p.set(b,[0,0]);
|
||||||
|
return flatBeltCaps(b-1,p)===3 && before===JSON.stringify([Array.from(blockP),Array.from(blockV)]);
|
||||||
|
})()`));
|
||||||
|
check('competing diagonal and flat neighbours keep every returned join reciprocal', run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
const b=10*BW+10;
|
||||||
|
blockP[b]=BP_BELT_DOWN; blockP[b+BW+1]=BP_BELT_DOWN; blockP[b+BW+2]=BP_FILTER;
|
||||||
|
blockP[b-1]=BP_FILTER; blockP[b-BW-1]=BP_BELT_DOWN;
|
||||||
|
for(let y=8;y<14;y++) for(let x=8;x<14;x++) for(let e=0;e<2;e++) {
|
||||||
|
const j=endJoin(x,y,e);
|
||||||
|
if(j>=0 && endJoin((j>>1)%BW,((j>>1)/BW)|0,j&1)!==(y*BW+x)*2+e) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})()`));
|
||||||
|
done();
|
||||||
@@ -64,7 +64,7 @@ check('free end rounds its corners and stops at the end', run(`(() => {
|
|||||||
})()`));
|
})()`));
|
||||||
check('flat belts still meet diagonal screw elbows without an end cap', run(`(() => {
|
check('flat belts still meet diagonal screw elbows without an end cap', run(`(() => {
|
||||||
blockP[artB - 1] = BP_BELT;
|
blockP[artB - 1] = BP_BELT;
|
||||||
blockP[artB] = BP_SCREW; blockV[artB] = 4;
|
blockP[artB] = BP_SCREW; blockV[artB] = 5; // same-row flat at the high end
|
||||||
const joined = !(flatBeltCaps(artB - 1) & 2);
|
const joined = !(flatBeltCaps(artB - 1) & 2);
|
||||||
blockP[artB] = BP_BELT_DOWN;
|
blockP[artB] = BP_BELT_DOWN;
|
||||||
return joined;
|
return joined;
|
||||||
|
|||||||
Reference in New Issue
Block a user