191 lines
11 KiB
JavaScript
191 lines
11 KiB
JavaScript
// 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;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);
|
|
}
|
|
// 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();
|