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;})()`);
+101 -79
View File
@@ -7,29 +7,26 @@ 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]);
for (const v of c === BP_FILTER || isFilter(c) ? [0, 127, 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 };
if(code===screw)return {family:'screw',x:dirs[v][0],y:dirs[v][1],diagonal:!!(dirs[v][0]&&dirs[v][1])};
const x=run(`isFilter(${code})`)?(v&128?1:-1):(v&1?1:-1),y=run(`beltGrade(${code})`)*x;
return {family:'belt',x,y,diagonal:!!y};
}
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;
function directed(a,b,dx,dy) {
if(a.x*b.x+a.y*b.y<=0)return false;
if(a.family!==b.family&&((!a.x||!b.x)||(a.diagonal&&b.diagonal)))return false;
if(a.diagonal&&b.diagonal&&(a.x!==b.x||a.y!==b.y))return false;
return dx===a.x&&(dy===(a.family==='belt'&&a.y<0?0:a.y)||
(a.family==='belt'&&b.family==='belt'&&!a.y&&b.y<0&&dy===-1));
}
function oracle(a,b,dx,dy,ae,be) {
const ao=(a.x||a.y)>0?1:0,bo=(b.x||b.y)>0?1:0;
return ae===ao&&be!==bo?directed(a,b,dx,dy):ae!==ao&&be===bo&&directed(b,a,-dx,-dy);
}
let tableErrors = [], symmetryErrors = [], pairCount = 0;
for (const ac of cases) for (const bc of cases) {
@@ -66,22 +63,22 @@ function pair(label, a, b, dx, dy, expected) {
})()`) === 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);
for(const [label,av,bv,dx,dy,yes] of [
['H(right) → DR next same row',1,5,1,0,true],['DR → H(right) diagonally past',5,1,1,1,true],
['H(right) → UR next same row',1,4,1,0,true],['UR → H(right) diagonally past',4,1,1,-1,true],
['V(up) → UR directly above',0,4,0,-1,true],['V(up) → UL directly above',0,7,0,-1,true],
['UR → V(up) diagonally past',4,0,1,-1,true],['DR → V(down) diagonally past',5,2,1,1,true],
['DR → H same row stays free',5,1,1,0,false],['H → DR one right and lower stays free',1,5,1,1,false],
['V(up) → UR diagonally above-right stays free',0,4,1,-1,false]
])pair(label,[screw,av],[screw,bv],dx,dy,yes);
pair('uphill belt outlet stays on its row',[ids[3],1],[ids[1],1],1,0,true);
pair('uphill slope outlet also targets a slope on the same row',[ids[3],1],[ids[3],1],1,0,true);
pair('uphill slope leaves a diagonally adjacent slope free',[ids[3],1],[ids[3],1],1,-1,false);
pair('flat belt feeds uphill low end on same row',[ids[1],1],[ids[3],1],1,0,true);
pair('flat belt also feeds uphill low end from below',[ids[1],1],[ids[3],1],1,-1,true);
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']]]);
const a=10*BW+10,b=a+1, p=new Map([[a,[BP_BELT,1]],[b,[BP_SCREW,1,'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]);
@@ -93,56 +90,81 @@ check('non-transport pieces, invalid ends and world edges are open without row w
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('same-shape inlet continuation beats an elbow',run(`(()=>{
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
for(const [n,v] of [[b,4],[b+BW-1,4],[b-1,1]]){blockP[n]=BP_SCREW;blockV[n]=v;}
return endJoin(10,10,0)===(b+BW-1)*2+1&&endJoin(9,10,1)===-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]]]);
check('equal-shape inlet ties use top to bottom then left to right',run(`(()=>{
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
for(const [n,v] of [[b,1],[b-BW-1,5],[b+BW-1,4]]){blockP[n]=BP_SCREW;blockV[n]=v;}
return endJoin(10,10,0)===(b-BW-1)*2+1&&endJoin(9,11,1)===-1;
})()`));
const fs=require('fs');
run(fs.readFileSync('src/render/slope-belts.js','utf8'));
const setup=fs.readFileSync('src/render/setup.js','utf8');
run(setup.slice(setup.indexOf('function beltLike('),setup.indexOf('\n',setup.indexOf('function beltLike('))));
check('filter frames open at an uphill low-end feed and compatible screws',run(`(()=>{
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
blockP[b]=BP_FILTER;blockV[b]=255;blockP[b+1]=BP_BELT_UP;blockV[b+1]=1;
if(!beltLike(b,1))return false;
blockP[b+1]=BP_SCREW;blockV[b+1]=1;if(!beltLike(b,1))return false;
blockV[b+1]=3;return !beltLike(b,1);
})()`));
check('planned flat caps and slope continuations use the shared ends',run(`(()=>{
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
const p=new Map([[b,[BP_BELT_DOWN,1]],[b-1,[BP_FILTER,255]],[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)]);
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;
check('a flat belt picks the same-row uphill inlet before the below-low-end exception',run(`(()=>{
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
for(const n of [b,b+1,b-BW+1]){blockP[n]=n===b?BP_BELT:BP_BELT_UP;blockV[n]=1;}
return endJoin(10,10,1)===(b+1)*2&&endJoin(11,9,0)===-1;
})()`));
check('same-row uphill low end draws the drop in placed and planned geometry',run(`(()=>{
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
const p=new Map([[b,[BP_BELT_UP,1]],[b-1,[BP_BELT,1]]]);
const j=slopeBeltConnections(10,10,BP_BELT_UP,p);
return j===129&&slopeBeltShape(40.5,43,b,j,BP_BELT_UP)<0&&flatBeltCaps(b-1,p)===1;
})()`));
check('uphill same-row slope joins draw a high elbow and a low drop',run(`(()=>{
blockP.fill(0);blockV.fill(0);const b=10*BW+10,p=new Map([[b,[BP_BELT_UP,1]],[b+1,[BP_BELT_UP,1]]]);
return slopeBeltConnections(10,10,BP_BELT_UP,p)===2&&slopeBeltConnections(11,10,BP_BELT_UP,p)===129;
})()`));
// Downhill face-to-face transfers (screens always travel downhill when placed).
for(const grade of [-1,1])for(const filter of [false,true])for(const outgoing of [false,true])for(const joined of [false,true]) {
flatWorld(run,96,96,92);
const d=grade,c=filter?(grade<0?'BP_FILTER_UP':'BP_FILTER_DOWN'):(grade<0?'BP_BELT_UP':'BP_BELT_DOWN');
const r=run(`(()=>{
const d=${d},c=${c},out=${outgoing},bx=10,by=10;
placeBlock(c,${filter?(d>0?128:0):(d>0?1:0)},bx,by,true);
const fx=bx+(out?d:-d),fy=by+(out?1:0)+(${joined}?0:out?-1:1);
placeBlock(BP_BELT,d>0?1:0,fx,fy,true);
const sx=bx*BS+(d>0?3:0),ix=bx*BS+(d>0?0:3),x=out?sx:ix-d;
const y=out?beltFaceY(c,bx,by,sx):fy*BS;
const targetY=out?fy*BS:beltFaceY(c,bx,by,ix);
setCell((y-1)*W+x,SAND,20,0);tickStamp++;
carryColumn((y-1)*W+x,d,null);
return [endJoin(bx,by,out?(d>0?1:0):(d>0?0:1))>=0,mat[(targetY-1)*W+x+d]===SAND];
})()`);
check(`grade ${grade} ${filter?'screen':'belt'} ${outgoing?'outlet':'inlet'} ${joined?'joined':'free'} sand handoff agrees`,r[0]===joined&&r[1]===joined,r);
}
// The same-row low-end feed drops onto the face before the uphill cleats carry it.
for(const grade of [-1,1])for(const below of [false,true]) {
flatWorld(run,160,128,120);
const d=-grade,code=grade<0?'BP_BELT_UP':'BP_BELT_DOWN';
const r=run(`(()=>{
const d=${d},c=${code},v=${d>0?1:0},bx=20,by=20,fy=by+${below?1:0};
placeBlock(BP_BELT,d>0?1:0,bx-d,fy,true);
for(let k=0;k<4;k++)placeBlock(c,v,bx+k*d,by-k,true);
rebuildPower();const source=fy*BS-1,x=(bx-d)*BS+(d>0?3:0);
setCell(source*W+x,SAND,20,0);
let seen=false;
for(let k=0;k<320;k++) {simTick();for(let i=0;i<N;i++)if(mat[i]===SAND&&((i/W|0)<by*BS-4))seen=true;}
return [endJoin(bx-d,fy,d>0?1:0)>=0,seen];
})()`);
check(`grade ${grade} belt low end accepts sand from ${below?'below':'same row'} and carries it up`,r[0]&&r[1],r);
}
done();
+3 -3
View File
@@ -10,10 +10,10 @@ for (const code of ['BP_BELT_UP', 'BP_BELT_DOWN', 'BP_FILTER_UP', 'BP_FILTER_DOW
run(`
mat.fill(EMPTY); part.fill(0); blockP.fill(0); blockV.fill(0);
var artCode = ${code}, artGrade = beltGrade(artCode), artB = 12 * BW + 12;
for (let k = -2; k <= 2; k++) placeBlock(artCode, 1, 12 + k, 12 + artGrade * k, true);
for (let k = -2; k <= 2; k++) placeBlock(artCode, artGrade > 0 ? 1 : 0, 12 + k, 12 + artGrade * k, true);
prepareSlopeBelts(0, 0, W, H);
`);
check(`${code}: diagonal neighbours continue the band`, run('(slopeBeltJoins[artB] & 12) === 12'));
check(`${code}: downhill diagonal neighbours continue the band`, run('(slopeBeltJoins[artB] & 12) === 12'));
check(`${code}: corner seals use the same band`, run(`(() => {
const i = slopeSeal(artCode, 12, 12);
return i < 0 || slopeBeltAt(i, i % W, (i / W) | 0) >= 0;
@@ -35,7 +35,7 @@ for (const code of ['BP_BELT_UP', 'BP_BELT_DOWN', 'BP_FILTER_UP', 'BP_FILTER_DOW
for (let end = 0; end < 2; end++) {
const side = end ? 1 : -1;
removeBlock(12 + side, 12 + side * artGrade, true);
placeBlock(BP_BELT, 1, 12 + side, 12 + (end ? artGrade > 0 : artGrade < 0), true);
placeBlock(BP_BELT, artGrade > 0 ? 1 : 0, 12 + side, 12 + (end ? artGrade > 0 : artGrade < 0), true);
}
prepareSlopeBelts(0, 0, W, H);
`);
+37 -2
View File
@@ -97,7 +97,7 @@ with sync_playwright() as p:
}''')
check('sidewall stays behind the grain colour in pixels and smooth',
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
# Shared joins: E elbows bend inside the straight block; other placements keep collars.
# Travel joins: edge elbows in diagonals, E elbows in straight receivers, otherwise collars.
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("() => {" + (root / "tools/scenes/joins.js").read_text() + "}")
@@ -153,7 +153,7 @@ with sync_playwright() as p:
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))) {
for(const look of ['pixel','smooth'])for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW && j.joined && j.kind>=5 && (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;
@@ -196,6 +196,41 @@ with sync_playwright() as p:
} finally {performance.now=now;}
}''')
check('smooth: trees behind a straight screw cannot become elbow join bits', not tree_tags, tree_tags)
drops = ev("""() => {
const detail=slopeDetailAt,now=performance.now.bind(performance),frozen=now(),out={};
const j=JOIN_CASES.find(j=>j.v===4&&j.end===0&&j.code===BP_BELT&&j.spot===0);
try {
slopeDetailAt=()=>0;performance.now=()=>frozen;
const sample=()=>{const p=worldToScreenPx(j.bx*BS+0.4,j.by*BS+2.5);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
for(const look of ['pixel','smooth']) {
SET.look=look;SM.scale=1;drawWorld();const joined=sample();
removeBlock(j.nx,j.ny);drawWorld();const free=sample();
placeBlock(BP_BELT,j.straight,j.nx,j.ny,true);
out[look]=[joined,free];
}
return out;
} finally {slopeDetailAt=detail;performance.now=now;}
}""")
for look, (joined, free) in drops.items():
check(f'{look}: the same-row uphill inlet draws its drop above the low face', joined != free and joined[2] < free[2], [joined, free])
filter_plans = ev("""() => {
const saved={...GAME},original=slopeBeltConnections,seen=[];
try {
GAME.tool=TOOLS.findIndex(t=>t.id==='build');GAME.pieceIdx=PIECES.findIndex(p=>p.id==='filter');
GAME.selDrag=null;GAME.paste=false;GAME.filterCfg={m:[],inv:0};GAME.pieceDir={filter:1};
slopeBeltConnections=(x,y,c,p)=>{
if(p)seen.push([c,p.get(y*BW+x)?.[1]]);
return original(x,y,c,p);
};
for(const grade of [-1,1]) {
GAME.drag={bx:50,by:30};GAME.aim={x:52*BS,y:(30+2*grade)*BS};
drawPreviews();
}
return seen;
} finally {slopeBeltConnections=original;Object.assign(GAME,saved);}
}""")
check('filter drag previews pass stored travel bits to the join rule',
bool(filter_plans) and all(v == (128 if c == ev('BP_FILTER_DOWN') else 0) for c, v in filter_plans), filter_plans)
# a browser without hardware drawing gets the pixel look and a note
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
pg2.add_init_script('localStorage.clear();')