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
+59 -44
View File
@@ -12,8 +12,8 @@ function line(v, chain = false) {
run(`for(let k=0;k<10;k++) placeBlock(BP_SCREW,${v},${bx}+k*${dx},${by}+k*${dy},true);`);
let ty = by, tail = bx, head = bx + 9 * dx;
if (chain) {
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); }`);
ty += dy<0?1:0; tail -= 3 * dx; head += 3 * dx;
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 + 10 * dy},true); }`);
tail -= 3 * dx; head += 3 * dx;
}
return { dx, dy, bx, by, ty, tail, head, hy: by + 9 * dy };
}
@@ -50,34 +50,30 @@ for (const v of [4, 5, 6, 7]) {
const joined = drain(c,500);
check(`direction ${v}: straight to diagonal to straight`,joined.out===16 && joined.leaks===0,JSON.stringify(joined));
}
// Every neighbouring block, including the old edge probes and E, in both directions.
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();
run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${sv},${nx},${ny},true);`);
const kind=joined?(oy===0?(side>0?2:4):vertical?6:5):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};})()`));
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;})()`));
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;})()`));
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'));
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;})()`);
check(`${label}: grains ${forward?'out':'in'} ${joined?'cross':'stay separate'}`,joined?crossed===16:crossed===0,crossed);
{
run('for(let i=0;i<N;i++)if(mat[i]===SAND)setCell(i,EMPTY,20,0);');
putBar(sx*4,sy*4+1);
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;})()`);
check(`${label}: bar ${forward?'out':'in'} ${joined?'crosses':'stays separate'}`,bar===joined);
}
}
// Every straight direction and neighbour offset, with actual transfers in both source orders.
for(const v of [4,5,6,7])for(const sv of [0,1,2,3])for(const [ox,oy] of [[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]]) {
const [dx,dy]=run(`SCREW_DIRS[${v}]`),[sxv,syv]=run(`SCREW_DIRS[${sv}]`),vertical=!sxv;
const out=ox===dx&&oy===dy&&dx*sxv+dy*syv>0;
const incoming=ox===-sxv&&oy===-syv&&dx*sxv+dy*syv>0;
const joined=out||incoming,end=out?(dx>0?1:0):(dx>0?0:1),nx=20+ox,ny=20+oy;
const label=`diagonal ${v} straight ${sv} offset ${ox},${oy}`;
fresh();run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${sv},${nx},${ny},true);`);
const kind=joined?(oy===0?(ox>0?2:4):ox===0?(oy<0?1:3):vertical?6:5):0;
check(`${label}: drawing agrees with travel joins`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});return pieceJoins(20,20)===${joined?1<<end:0}&&${joined?`(j>>${end*3}&7)===${kind}`:'(j&192)===192'};})()`));
check(`${label}: complete ghost plan matches placed joins`,run(`(()=>{
const j=diagonalScrewJoins(20,20,${v}),sj=straightScrewJoins(${nx},${ny},${sv});removeBlock(20,20);removeBlock(${nx},${ny});
const p=new Map([[20*BW+20,[BP_SCREW,${v}]],[${ny}*BW+${nx},[BP_SCREW,${sv}]]]);
return diagonalScrewJoins(20,20,${v},p)===j&&straightScrewJoins(${nx},${ny},${sv},p)===sj;
})()`));
for(const forward of [true,false]) {
fresh();run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${sv},${nx},${ny},true);rebuildPower();`);
const sx=forward?20:nx,sy=forward?20:ny,tx=forward?nx:20,ty=forward?ny:20,passes=forward?out:incoming;
rect(run,sx*4,sy*4,sx*4+3,sy*4+3,run('SAND'));
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;})()`);
check(`${label}: grains ${forward?'out':'in'} ${passes?'cross':'stay separate'}`,passes?crossed===16:crossed===0,crossed);
run('for(let i=0;i<N;i++)if(mat[i]===SAND)setCell(i,EMPTY,20,0);');putBar(sx*4,sy*4+1);
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;})()`);
check(`${label}: bar ${forward?'out':'in'} ${passes?'crosses':'stays separate'}`,bar===passes);
}
}
fresh();
@@ -86,21 +82,40 @@ 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.
// Flat belts and filters receive diagonal outlets in E, on their carrying 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();`);
const slope=v===4||v===6?-1:1,dy=side*slope,nx=20+side,ny=20+dy;
const dv=(side>0)===(v<6)?v:(v<6?v+2:v-2),end=side>0?1:0,kind=5;
run(`placeBlock(BP_SCREW,${dv},20,20,true);placeBlock(${code},${code==='BP_FILTER'?(side>0?128:0):(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.
// Flat carriers enter every diagonal at its same-row inlet; blocked mouths must wait safely.
for(const v of [4,5,6,7])for(const filter of [false,true]) {
fresh();const dx=run(`SCREW_DIRS[${v}][0]`),nx=20+dx;
run(`placeBlock(${filter?'BP_FILTER':'BP_BELT'},${filter?(dx>0?128:0):(dx>0?1:0)},20,20,true);placeBlock(BP_SCREW,${v},${nx},20,true);rebuildPower();`);
putBar(80,78);
run(`setCell(80*W+${nx*4},SAND,20,0);`);
check(`${filter?'filter':'belt'} → diagonal ${v}: a blocked joined inlet holds the bar`,run(`(()=>{
const e=ents[0];entMark(e,0);tick=8;entCurM=e.m;const held=entBeltScrewEnter(e);entMark(e,1);
return held&&e.ix===80&&e.iy===78;
})()`));
run(`setCell(80*W+${nx*4},EMPTY,20,0);`);
check(`${filter?'filter':'belt'} → diagonal ${v}: clearing the inlet resumes the drop`,run(`(()=>{
const e=ents[0];entMark(e,0);const moved=entBeltScrewEnter(e);entMark(e,1);
return moved&&e.ix===${nx*4}&&e.iy===80&&!!entScrewAt(e.ix,e.iy);
})()`));
fresh();run(`placeBlock(${filter?'BP_FILTER':'BP_BELT'},${filter?(dx>0?128:0):(dx>0?1:0)},20,20,true);placeBlock(BP_SCREW,${v<6?v+2:v-2},${nx},20,true);rebuildPower();`);
putBar(80,78);
check(`${filter?'filter':'belt'} → diagonal ${v}: incompatible travel never snaps a bar`,run('(()=>{const e=ents[0];entMark(e,0);const entered=entBeltScrewEnter(e);entMark(e,1);return !entered&&e.ix===80&&e.iy===78;})()'));
}
// The removed straight-to-diagonal corner path stays open.
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'));
check('a straight mouth never takes the removed corner shortcut',run('straightScrewNext(82*W+83,P_SCREW_R)===82*W+84'));
// A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz.
const rates = [];
for (const v of [0, 4, 5]) {
@@ -125,9 +140,9 @@ console.log('Saturated throughput, ten blocks, grains/s: straight/up-diagonal/do
check('diagonals match straight throughput per block',rates.every(n=>n===120),rates);
// A belt's last carrying cell feeds the tail side of an upward diagonal screw.
const belt=line(4);
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,33,true);');
rect(run,39,100,39,131,run('STONE'));
rect(run,40,120,55,131,run('SAND'));
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,32,true);');
rect(run,39,100,39,127,run('STONE'));
rect(run,40,116,55,127,run('SAND'));
const beltOut=run(`(()=>{let out=0;for(let k=0;k<1000;k++){simTick();for(let f=flyers.length-1;f>=0;f--)if(flyers[f].m===SAND && flyers[f].x>=104){out++;flyers.splice(f,1);}}return out;})()`);
check('a belt feeds sand into a diagonal tail and out its head',beltOut>150,beltOut);
for(const v of [4,5,6,7]) {
@@ -157,9 +172,9 @@ for(const v of [4,5,6,7]) for(const chain of [false,true]) {
const r=watchBars(s);
check(`bars travel diagonal ${v}${chain?' and both straight joins':''} without overlap`,r.out===2 && !r.overlap,JSON.stringify(r));
}
// Vertical joins live in E at both ends, then continue along the lift axis.
// A lift feeds a diagonal directly ahead; the diagonal feeds the next lift in E.
for (const v of [4,5,6,7]) {
const s=line(v), vv=s.dy<0?0:2, tailX=s.bx-s.dx, headX=s.head+s.dx;
const s=line(v), vv=s.dy<0?0:2, tailX=s.bx, headX=s.head+s.dx;
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(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)};})()`);
@@ -171,10 +186,10 @@ for (const v of [4,5,6,7]) {
}
for(const v of [4,5]) {
const s=line(v);
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,'+(s.by+1)+',true);spawnBar(BARCU,44,'+(s.by*4+2)+',0,0);');
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,'+s.by+',true);spawnBar(BARCU,44,'+(s.by*4-2)+',0,0);');
check(`a belt feeds a bar into diagonal ${v}`,watchBars(s).out===1);
// An ascending screw discharges above the receiving belt; a descending one lands on its top.
const b=line(v), beltY=v===4?b.hy:b.hy+1;
// Both diagonal outlet grades discharge onto the joined belt in E.
const b=line(v), beltY=b.hy+b.dy;
run(`for(let k=1;k<=8;k++)placeBlock(BP_BELT,1,${b.head}+k,${beltY},true);`);
rect(run,b.bx*4,b.by*4,b.bx*4+3,b.by*4+3,run('SAND'));
const onBelt=run(`(()=>{let seen=false;for(let k=0;k<300;k++){simTick();for(let x=${(b.head+2)*4};x<${(b.head+8)*4};x++)if(mat[${beltY*4-1}*W+x]===SAND)seen=true;}return seen;})()`);