// 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, 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)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 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) { 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]'); 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,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]); 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; })()`)); 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('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)]); })()`)); 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 as two capped ends 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===0&&endJoin(10,10,0,p)>=0&&flatBeltCaps(b-1,p)===3; })()`)); check('uphill slopes side by side on one row are joined but drawn as two capped ends',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 endJoin(10,10,1,p)>=0&&slopeBeltConnections(10,10,BP_BELT_UP,p)===0&&slopeBeltConnections(11,10,BP_BELT_UP,p)===0; })()`)); // 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;i0?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); } // Snapping (t67 phase 4): a drag moves one block up or down only to make a seamless join // involving a diagonal piece, and only when every block there is free. const snap = (setup, code, v, cells) => { flatWorld(run, 96, 96, 92); return run(`(()=>{${setup};return joinSnap(${code},${v},${JSON.stringify(cells)});})()`); }; check('an uphill belt started beside a flat belt on the same level snaps up one to join seamlessly', snap('placeBlock(BP_BELT,1,10,10,true)', 'BP_BELT_UP', 1, [[11,10],[12,9],[13,8]]) === -1); check('the snapped uphill belt then joins the flat belt', run('(()=>{placeBlocks(BP_BELT_UP,1,[[11,9],[12,8],[13,7]]);return endJoin(10,10,1)===(9*BW+11)*2+0||endJoin(11,9,0)>=0;})()')); check('an uphill belt already one block up stays put', snap('placeBlock(BP_BELT,1,10,10,true)', 'BP_BELT_UP', 1, [[11,9],[12,8]]) === 0); check('no snap when a block above is taken; the same-row drop join stays', snap('placeBlock(BP_BELT,1,10,10,true);placeBlock(BP_WALL,RSTONE,12,8,true)', 'BP_BELT_UP', 1, [[11,10],[12,9]]) === 0); check('a flat belt beside another flat belt one row off never jumps', snap('placeBlock(BP_BELT,1,10,10,true)', 'BP_BELT', 1, [[11,11],[12,11],[13,11]]) === 0); check('a flat belt started level with a downhill belt low end snaps down one (t58)', snap('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true)', 'BP_BELT', 1, [[12,11],[13,11]]) === 1); check('a downhill screw started one block right and one lower than a screw end snaps up to the same row', snap('placeBlock(BP_SCREW,1,10,10,true)', 'BP_SCREW', 5, [[11,11],[12,12]]) === -1); check('a screw that already joins stays put', snap('placeBlock(BP_SCREW,1,10,10,true)', 'BP_SCREW', 5, [[11,10],[12,11]]) === 0); check('pieces with no join ends never snap', snap('', 'BP_PIPE', 0, [[11,10]]) === 0); done();