Joins follow the travel direction: the next piece goes right after the end (t67 phase 2b)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 14:09:28 -04:00
co-authored by Claude Opus 5.5
parent fd35bf334f
commit 45538cc18e
16 changed files with 363 additions and 236 deletions
+4 -16
View File
@@ -185,22 +185,10 @@ 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;
const q = part[next];
if (!(SCREW_DX[q] && SCREW_DY[q])) return next;
const join = endJoin(x >> 2, y >> 2, dx + dy > 0 ? 1 : 0);
return join >= 0 && (join >> 1) === (yy >> 2) * BW + (xx >> 2) ? next : -1;
}
// Four lanes cross a block in four beats; only a mutual geometric join can receive them.
function diagonalScrewNext(i, p) {
+18 -8
View File
@@ -93,13 +93,6 @@ function entScrewTick(e, p) {
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;
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const i = entScrewEdge(e, dx, dy, k);
@@ -149,6 +142,23 @@ function entDiagonalScrewTick(e, p) {
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++; }
}
// A same-row flat belt drops its bar into a joined screw, just as grains fall off its face.
function entBeltScrewEnter(e) {
if (e.ix & 3) return false;
const y = e.iy + BAR_H;
if (y >= H || (y & 3)) return false;
const b = (y >> 2) * BW + (e.ix >> 2), c = blockP[b], v = blockV[b];
if (c !== BP_BELT && c !== BP_FILTER) return false;
if (c === BP_FILTER && filterPassAt(e.m, y * W + e.ix)) return false;
if (netSat(y * W + e.ix) <= 0) return false;
const d = c === BP_FILTER ? (v & 128 ? 1 : -1) : (v & 1 ? 1 : -1);
const join = endJoin(b % BW, (b / BW) | 0, d > 0 ? 1 : 0);
if (join < 0 || blockP[join >> 1] !== BP_SCREW) return false;
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
const nb = join >> 1;
if (!(tick & 1) && entScrewMove(e, nb % BW * BS, ((nb / BW) | 0) * BS, true, true)) stats.moved++;
return true;
}
// Bars stay level; their highest contact follows the face one grain per belt beat.
function entSlopeBeltTick(e) {
const bottom = e.iy + BAR_H;
@@ -188,7 +198,7 @@ function entTick() {
let ix = e.ix; const iy = e.iy;
const screw = entScrewAt(ix, iy);
if (screw) entScrewTick(e, screw);
else if (!entSlopeBeltTick(e)) {
else if (!entBeltScrewEnter(e) && !entSlopeBeltTick(e)) {
// what is underneath?
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
if (!support) {
+41 -39
View File
@@ -1,46 +1,48 @@
// 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];
// Travel-directed transport ends. Precomputed probes; world/plan queries allocate nothing.
// End indices stay geometric: left/right, or up/down for vertical screws.
const JOIN_ENDS = [[]];
for (const [kind, tx, ty] of [
...SCREW_DIRS.map(([x,y]) => [x && y ? 'screw-diagonal' : x ? 'screw-horizontal' : 'screw-vertical',x,y]),
['belt-flat',-1,0], ['belt-flat',1,0],
['belt-sloped',-1,1], ['belt-sloped',1,-1],
['belt-sloped',-1,-1], ['belt-sloped',1,1]
]) {
const outlet = (tx || ty) > 0 ? 1 : 0;
JOIN_ENDS.push([0,1].map(e => ({
dx: (e ? 1 : -1) * Math.abs(tx), dy: (e ? 1 : -1) * (tx ? ty * tx : 1),
diagonal: !!(tx && ty), kind, tx, ty, outlet: e === outlet, targets: []
})));
}
// Compile the diagonal-end rule and its inverse into bounded neighbour probes.
for (const ends of JOIN_ENDS) for (const end of ends) {
function pieceEnds(code, v) {
if (code === BP_SCREW) return JOIN_ENDS[v >= 0 && v < 8 ? v + 1 : 0];
if (!isBelt(code) && !isFilter(code)) return JOIN_ENDS[0];
const d = isFilter(code) ? (v & 128 ? 1 : 0) : (v & 1);
return JOIN_ENDS[9 + d + (beltGrade(code) < 0 ? 2 : beltGrade(code) > 0 ? 4 : 0)];
}
for (const ends of JOIN_ENDS) for (let e = 0; e < ends.length; e++) {
const a = ends[e];
if (!a.outlet) continue;
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;
const b = others[n], screw = a.kind.startsWith('screw'), otherScrew = b.kind.startsWith('screw');
if (b.outlet || a.tx * b.tx + a.ty * b.ty <= 0) continue;
if (screw !== otherScrew && (a.kind === 'screw-vertical' || b.kind === 'screw-vertical' ||
(a.diagonal && b.diagonal))) continue;
if (a.diagonal && b.diagonal && (a.tx !== b.tx || a.ty !== b.ty)) continue;
const dy = !screw && a.ty < 0 ? 0 : a.ty;
const offsets = [[a.tx, dy]];
// Grains on a flat belt below an uphill low end already reach its carrying face.
if (!screw && !otherScrew && !a.ty && b.ty < 0) offsets.push([a.tx, -1]);
for (const [dx, yy] of offsets) {
a.targets.push({ dx, dy: yy, ends: others, end: n });
b.targets.push({ dx: -dx, dy: -yy, ends, end: e });
}
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));
}
for (const ends of JOIN_ENDS) for (const end of ends) {
const sameShape = t => t.ends[t.end].kind === end.kind &&
t.ends[t.end].tx === end.tx && t.ends[t.end].ty === end.ty;
end.targets.sort((a,b) => end.outlet ? Math.abs(a.dy - end.ty) - Math.abs(b.dy - end.ty) :
Number(sameShape(b)) - Number(sameShape(a)) || a.dy - b.dy || a.dx - b.dx);
}
// Result: neighbour block index * 2 + its end index; -1 means an open end.
function joinCandidate(bx, by, endIndex, planned) {