242 lines
17 KiB
JavaScript
242 lines
17 KiB
JavaScript
// Diagonal screws: sealed four-lane corner transfers, bars, joins and persistence.
|
|
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
|
|
const fs = require('fs');
|
|
const { run } = load();
|
|
const { check, done } = suite();
|
|
run(fs.readFileSync('src/render/diagonal-screws.js', 'utf8'));
|
|
run(fs.readFileSync('src/render/world.js', 'utf8'));
|
|
function fresh() { flatWorld(run, 256, 192, 176); run('RS=7; nextPowerBuild=0; money=10000; AMB.fill(20); temp.fill(20); hot.fill(0);'); }
|
|
function line(v, chain = false) {
|
|
fresh();
|
|
const [dx, dy] = run(`SCREW_DIRS[${v}]`), bx = dx > 0 ? 16 : 36, by = dy < 0 ? 32 : 12;
|
|
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;
|
|
}
|
|
return { dx, dy, bx, by, ty, tail, head, hy: by + 9 * dy };
|
|
}
|
|
function drain(s, steps, feed = false) {
|
|
return run(`(() => {
|
|
let out=0, leaks=0, added=0;
|
|
for(let k=0;k<${steps};k++) {
|
|
${feed ? `for(let l=0;l<4;l++) { const i=(${s.ty ?? s.by}*4+l)*W+${s.tail}*4+${s.dx > 0 ? 0 : 3}; if(mat[i]===EMPTY) {setCell(i,SAND,20,0);added++;} }` : ''}
|
|
simTick();
|
|
for(let i=0;i<N;i++) if(mat[i]===SAND && !IS_SCREW[part[i]]) {
|
|
const x=i%W; if(${s.dx}>0 ? x>=${(s.head + 1) * 4} : x<${s.head * 4}) out++; else leaks++;
|
|
setCell(i,EMPTY,20,0);
|
|
}
|
|
for(const f of flyers) if(f.m===SAND) {
|
|
if(${s.dx}>0 ? f.x>=${(s.head + 1) * 4} : f.x<${s.head * 4}) out++; else leaks++;
|
|
}
|
|
flyers=flyers.filter(f=>f.m!==SAND);
|
|
}
|
|
return {out,leaks,added,inside:mat.reduce((n,m)=>n+(m===SAND),0)};
|
|
})()`);
|
|
}
|
|
for (const v of [4, 5, 6, 7]) {
|
|
const s = line(v);
|
|
check(`direction ${v} uses appended part ids and marks power`,run(`part[${s.by * 4}*W+${s.bx * 4}]===${v + 7} && blockV[${s.by}*BW+${s.bx}]===${v} && pcells.size===160`));
|
|
// Fill every lane, in every block: a corner or side leak is counted before it can fall away.
|
|
run('for(let i=0;i<N;i++) if(IS_SCREW[part[i]]) setCell(i,SAND,20,0);');
|
|
const r = drain(s, 300);
|
|
check(`direction ${v} carries sand through ten blocks and out the head`, r.out === 160, JSON.stringify(r));
|
|
check(`direction ${v} has no corner or side spills and conserves grains`, r.leaks === 0 && r.out+r.inside === 160,JSON.stringify(r));
|
|
run('rebuildPower();');
|
|
check(`direction ${v} joins corner-touching power networks`,run('nets.length===2 && nets[1].nt===160'));
|
|
const c = line(v, true);
|
|
rect(run,c.tail*4,c.ty*4,c.tail*4+3,c.ty*4+3,run('SAND'));
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fresh();
|
|
run('placeBlock(BP_SCREW,1,20,20,true);placeBlock(BP_SCREW,0,21,20,true);rebuildPower();');
|
|
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.
|
|
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();`);
|
|
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.
|
|
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'));
|
|
// A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz.
|
|
const rates = [];
|
|
for (const v of [0, 4, 5]) {
|
|
let s;
|
|
if (v) s=line(v);
|
|
else {
|
|
fresh();run('for(let by=23;by<=32;by++) placeBlock(BP_SCREW,0,16,by,true);');
|
|
s={dx:0,tail:16,by:32,head:16};
|
|
}
|
|
const result = v ? (drain(s,80,true),drain(s,600,true)) : run(`(() => {
|
|
let out=0;
|
|
for(let k=0;k<680;k++) {
|
|
for(let l=0;l<4;l++) {const i=131*W+64+l;if(mat[i]===EMPTY)setCell(i,SAND,20,0);}
|
|
simTick();
|
|
if(k>=80) out+=flyers.filter(f=>f.m===SAND).length;
|
|
flyers=[];
|
|
}return {out};
|
|
})()`);
|
|
rates.push(result.out/10);
|
|
}
|
|
console.log('Saturated throughput, ten blocks, grains/s: straight/up-diagonal/down-diagonal '+rates.join('/'));
|
|
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'));
|
|
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]) {
|
|
const s=line(v);
|
|
rect(run,s.bx*4,s.by*4-8,s.bx*4+3,s.by*4-1,run('WATER'));
|
|
ticks(run,400);
|
|
check(`water flows freely through diagonal ${v}`,count(run,run('WATER'),0,0,255,172)===0 && count(run,run('WATER'))===32,count(run,run('WATER')));
|
|
}
|
|
// Bars keep their horizontal shape, with no saved ride state.
|
|
function putBar(x,y) {
|
|
run(`{ const e=spawnBar(BARCU,20,20,0,0); entMark(e,0); e.x=e.ix=${x}; e.y=e.iy=${y}; entMark(e,1); }`);
|
|
}
|
|
function watchBars(s, n=600) {
|
|
return run(`(()=>{let out=0,overlap=false;for(let k=0;k<${n};k++){
|
|
simTick();
|
|
for(let a=0;a<ents.length;a++)for(let b=0;b<a;b++) {
|
|
const e=ents[a],f=ents[b];if(e.ix<f.ix+4 && f.ix<e.ix+4 && e.iy<f.iy+2 && f.iy<e.iy+2)overlap=true;
|
|
}
|
|
if(mat.some((m,i)=>occ[i] && (PH[m]===PH_SOLID || PH[m]===PH_POWDER)))overlap=true;
|
|
for(let a=ents.length-1;a>=0;a--) {const e=ents[a];if(${s.dx}>0 ? e.ix>=${(s.head + 1) * 4} : e.ix+4<=${s.head * 4}) {out++;removeEnt(e);} }
|
|
}return {out,overlap,left:ents.length};})()`);
|
|
}
|
|
for(const v of [4,5,6,7]) for(const chain of [false,true]) {
|
|
const s=line(v,chain);
|
|
putBar(s.tail*4,s.ty*4);
|
|
putBar(s.tail*4,s.ty*4+2); // a queued bar must wait for the first one
|
|
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.
|
|
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;
|
|
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)};})()`);
|
|
check(`a bar crosses vertical joins at diagonal ${v}`,r.out && !r.overlap,JSON.stringify(r));
|
|
run('for(const e of ents.slice())removeEnt(e);');
|
|
rect(run,tailX*4,(s.by-3*s.dy)*4,tailX*4+3,(s.by-3*s.dy)*4+3,run('SAND'));
|
|
const grain=run(`(()=>{let out=0,leak=0;for(let k=0;k<500;k++){simTick();for(const f of flyers)if(f.m===SAND){if(${s.dy}<0?f.y<${(s.hy-3)*4}:f.y>=${(s.hy+4)*4})out++;else leak++;}flyers=[];}return {out,leak};})()`);
|
|
check(`sand crosses vertical joins at diagonal ${v}`,grain.out===16 && !grain.leak,JSON.stringify(grain));
|
|
}
|
|
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);');
|
|
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;
|
|
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;})()`);
|
|
check(`diagonal ${v} unloads sand onto a belt`,onBelt);
|
|
run('for(let i=0;i<N;i++)if(mat[i]===SAND)setCell(i,EMPTY,20,0);flyers=[];');
|
|
putBar(b.bx*4,b.by*4+1);
|
|
check(`diagonal ${v} unloads a bar onto a belt`,run(`(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=${(b.head+2)*4} && ents[0].iy===${beltY*4-2})return true;}return false;})()`));
|
|
}
|
|
const savedRide=line(4);
|
|
putBar(savedRide.bx*4,savedRide.by*4+1);ticks(run,24);
|
|
run('globalThis.rideSave=serializeWorld();deserializeWorld(rideSave.meta,rideSave.buffers);');
|
|
check('a bar saved mid-diagonal resumes its ride',watchBars(savedRide).out===1);
|
|
// Mouth obstruction must hold the bar, and clearing it must resume transport.
|
|
const blocked=line(4);
|
|
putBar(blocked.bx*4,blocked.by*4+1);
|
|
rect(run,(blocked.head+1)*4,(blocked.hy-1)*4,(blocked.head+2)*4-1,blocked.hy*4-1,run('STONE'));
|
|
ticks(run,300);
|
|
check('a blocked diagonal mouth holds its bar without wall overlap',run('ents.length===1 && entScrewAt(ents[0].ix,ents[0].iy) && !mat.some((m,i)=>m===STONE && occ[i])'));
|
|
rect(run,(blocked.head+1)*4,(blocked.hy-1)*4,(blocked.head+2)*4-1,blocked.hy*4-1,run('EMPTY'));
|
|
check('the diagonal bar resumes after the mouth clears',watchBars(blocked).out===1);
|
|
for(const v of [4,5,6,7]) {
|
|
fresh(); run(`placeBlock(BP_SCREW,${v},20,20,true);`);
|
|
run('globalThis.saved=serializeWorld();deserializeWorld(saved.meta,saved.buffers);');
|
|
check(`save/load keeps direction ${v} and power`,run(`blockV[20*BW+20]===${v} && part[80*W+80]===${v+7} && isPow[80*W+80]`));
|
|
run('globalThis.clip=copyRegion(80,80,83,83);bot.x=100;bot.y=80;undoRecord(\'paste\',()=>pasteGhost(clip,24,20));ghostTick();');
|
|
check(`copy/paste keeps direction ${v}`,run(`blockV[20*BW+24]===${v} && blockIntegrity(20*BW+24)===16`));
|
|
run('undoLast();');
|
|
check(`undo removes pasted diagonal ${v}`,run('blockP[20*BW+24]===0'));
|
|
run('globalThis.flipped=flipBlueprint(clip);');
|
|
check(`flip maps direction ${v}`,run('flipped.blocks[0][3]')===[7,6,5,4][v-4]);
|
|
run('undoRecord(\'cut\',()=>cutRegion(80,80,83,83));undoLast();');
|
|
check(`cut and undo restore direction ${v}`,run(`blockV[20*BW+20]===${v} && blockIntegrity(20*BW+20)===16`));
|
|
run('money=100;removeBlock(20,20);');
|
|
check(`diagonal ${v} refunds the straight screw price`,run('money')===125);
|
|
}
|
|
// Exercise the actual UI functions without starting a browser or simulating a replacement.
|
|
run(fs.readFileSync('src/ui/selection.js','utf8'));
|
|
run(fs.readFileSync('src/ui/actions.js','utf8'));
|
|
run('globalThis.GAME={pieceDir:{screw:0}};globalThis.hudDirty=false;globalThis.toolId=()=>\'build\';globalThis.piece=()=>PIECE_BY_ID.screw;globalThis.playSfx=()=>{};');
|
|
const cycle=run('(()=>{const ds=[];for(let k=0;k<8;k++){ds.push(GAME.pieceDir.screw);rotate();}return ds;})()');
|
|
check('R cycles all eight screw directions then wraps',cycle.join(',')==='0,1,2,3,4,5,6,7' && run('GAME.pieceDir.screw')===0,cycle);
|
|
for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
|
|
check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.screw,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v);
|
|
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
|
|
}
|
|
// The drawing can bridge a joint only in unoccupied air; transport parts stay square.
|
|
for (const v of [4,5,6,7]) {
|
|
flatWorld(run,240,192,176);
|
|
const rising = v===4 || v===6;
|
|
run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${v},21,${rising ? 19 : 21},true);`);
|
|
const x=84, y=rising ? 80 : 83;
|
|
check(`diagonal ${v} draws over the empty corner joint`,run(`diagonalScrewBlock(${x},${y},false)>=0 && part[${y}*W+${x}]===0`));
|
|
for(const obstacle of ['mat[j]=STONE','mat[j]=SAND','part[j]=P_PIPE','mach[j]=1','occ[j]=1','blockP[(j/W>>2)*BW+(j%W>>2)]=BP_WALL']) {
|
|
check(`diagonal ${v} leaves ${obstacle} visible`,run(`(()=>{const j=${y}*W+${x};${obstacle};const hidden=diagonalScrewBlock(${x},${y},true);mat[j]=EMPTY;part[j]=0;mach[j]=0;occ[j]=0;blockP[(${y}>>2)*BW+(${x}>>2)]=0;return hidden<0;})()`));
|
|
}
|
|
check(`diagonal ${v} keeps its continuation uncapped`,run(`(diagonalScrewJoins(20,20,${v}) & 128)===0 && (diagonalScrewJoins(21,${rising ? 19 : 21},${v}) & 64)===0`));
|
|
run(`removeBlock(21,${rising ? 19 : 21});`);
|
|
check(`diagonal ${v} has no stray corner after its end`,run(`diagonalScrewBlock(${x+1},${rising ? y-1 : y+1},false)===-1`));
|
|
}
|
|
fresh();
|
|
run('placeBlock(BP_SCREW,5,20,20,true);placeBlock(BP_SCREW,1,21,21,true);');
|
|
check('shared rule: downhill low end draws a horizontal elbow in E',run('(diagonalScrewJoins(20,20,5)>>3&7)===5'));
|
|
check('shared rule: downhill low end sends its last lane into E',run('diagonalScrewNext(83*W+83,P_SCREW_DR)===87*W+84'));
|
|
done();
|