// 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(); 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 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},true); placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${head}+k*${dx},${by + 9 * dy},true); }`); tail -= 3 * dx; head += 3 * dx; } return { dx, dy, bx, by, 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.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;i0 ? 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;i0?1:3); const sx=20+(fromDiagonal?0:-dx), sy=20+(fromDiagonal?0:-dy); const tx=20+(fromDiagonal?dx:0), ty=20+(fromDiagonal?dy:0); const source=fromDiagonal?v:sv, target=fromDiagonal?sv:v; const label=`corner ${vertical?'vertical':'horizontal'} ${v} ${fromDiagonal?'diagonal to straight':'straight to diagonal'}`; run(`placeBlock(BP_SCREW,${source},${sx},${sy},true);placeBlock(BP_SCREW,${target},${tx},${ty},true);rebuildPower();`); rect(run,sx*4,sy*4,sx*4+3,sy*4+3,run('SAND')); const grains=run(`(()=>{let crossed=0;for(let k=0;k<80;k++){ tick+=2;tickStamp++;machinesTick(); for(let i=0;i>2,y=(i/W|0)>>2; if(x===${tx} && y===${ty})crossed++; if(x!==${sx} || y!==${sy})setCell(i,EMPTY,20,0); }flyers=[]; }return crossed;})()`); check(`${label} does not pass grains`,grains===0,grains); run('mat.forEach((m,i)=>{if(m===SAND)setCell(i,EMPTY,20,0);});'); for(const lane of [0,1,2]) { putBar(sx*4,sy*4+lane); const bar=run(`(()=>{const e=ents[0];let crossed=false;for(let k=0;k<80;k++){ tick+=8;entMark(e,0);entScrewTick(e,SCREW_PARTS[${source}]);entMark(e,1); if((e.ix>>2)===${tx} && (e.iy>>2)===${ty})crossed=true; if(!entScrewAt(e.ix,e.iy))break; }removeEnt(e);return crossed;})()`); check(`${label} does not pass a bar in lane ${lane}`,!bar); } } // 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;aocc[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.by*4); putBar(s.tail*4,s.by*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 straight joins reach the same tails and heads too. for (const v of [4,5,6,7]) { const s=line(v), vv=s.dy<0?0:2; run(`for(let k=1;k<=3;k++){placeBlock(BP_SCREW,${vv},${s.bx},${s.by}-k*${s.dy},true);placeBlock(BP_SCREW,${vv},${s.head},${s.hy}+k*${s.dy},true);}`); putBar(s.bx*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)); // Sand from the vertical intake exits the far vertical head; no outside grains are left behind. run('for(const e of ents.slice())removeEnt(e);'); rect(run,s.bx*4,(s.by-3*s.dy)*4,s.bx*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{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)})`)); } // End elbows follow the adjoining straight piece's axis, regardless of flow direction. run(fs.readFileSync('src/render/diagonal-screws.js', 'utf8')); run(fs.readFileSync('src/render/world.js', 'utf8')); for (const v of [4,5,6,7]) for (const end of [-1,1]) for (let dir=0;dir<4;dir++) { fresh(); const slope=v===4 || v===6?-1:1, nx=20+end, ny=20+end*slope, shift=end>0?3:0; run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${dir},${nx},${ny},true);`); check(`corner ${v} end ${end} straight ${dir} keeps its free-end collar`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});return (j>>${shift}&7)===0 && !!(j&${end>0?128:64});})()`)); check(`corner ${v} end ${end} straight ${dir} leaves the free-end geometry unchanged`,run(`(()=>{const b=20*BW+20,j=diagonalScrewJoins(20,20,${v});removeBlock(${nx},${ny});const alone=diagonalScrewJoins(20,20,${v});placeBlock(BP_SCREW,${dir},${nx},${ny},true);return j===alone && [0.5,1.5,2.5,3.5].every(t=>diagonalScrewShape(82+${end}*2,80+t,b,${v},j)===diagonalScrewShape(82+${end}*2,80+t,b,${v},alone));})()`)); check(`corner ${v} end ${end} straight ${dir} planned joins match placed joins`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});removeBlock(${nx},${ny});return diagonalScrewJoins(20,20,${v},new Map([[${ny}*BW+${nx},[BP_SCREW,${dir}]]]))===j;})()`)); } for (const v of [4,5,6,7]) for (let dir=0;dir<4;dir++) for (const code of ['BP_SCREW','BP_BELT','BP_FILTER']) { if (code !== 'BP_SCREW' && !(dir & 1)) continue; fresh(); const [dx,dy]=run(`SCREW_DIRS[${dir}]`); run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(${code},${dir},${20+dx},${20+dy},true);`); const end = dx ? dx>0 : (v===4 || v===6 ? dy<0 : dy>0); check(`diagonal ${v} bends toward ${code} direction ${dir}`,run(`(diagonalScrewJoins(20,20,${v}) >> ${end?3:0} & 7)`)===dir+1); check(`planned ${code} direction ${dir} bends the diagonal ${v} ghost too`,run(`(()=>{removeBlock(${20+dx},${20+dy});const planned=new Map([[${20+dy}*BW+${20+dx},[${code},${dir}]]]);const j=diagonalScrewJoins(20,20,${v},planned);placeBlock(${code},${dir},${20+dx},${20+dy},true);return (j >> ${end?3:0} & 7)===${dir+1};})()`)); check(`diagonal ${v} meets ${code} direction ${dir} across its centred edge`,run(`(()=>{const b=20*BW+20,j=diagonalScrewJoins(20,20,${v});return [0.5,1.5,2.5,3.5].every(t=>diagonalScrewShape(${dx ? 82+dx*1.99 : '80+t'},${dy ? 82+dy*1.99 : '80+t'},b,${v},j)<=0);})()`)); } // 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`)); } done();