// 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, 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 }; } 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; } 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]'); 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); 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']]]); 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; })()`)); // 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('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]]]); 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('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; })()`)); done();