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
+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) {