Shared join module for transport pieces; belt drawing uses it (t67 phase 1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
||||
// 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();
|
||||
Reference in New Issue
Block a user