Screw conveyors join by the shared rule: low ends and verticals join in the corner block (t67 phase 2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+68
-63
@@ -3,23 +3,25 @@ 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 tail = bx, head = bx + 9 * dx;
|
||||
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},true); placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${head}+k*${dx},${by + 9 * dy},true); }`);
|
||||
tail -= 3 * dx; head += 3 * dx;
|
||||
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, tail, head, hy: by + 9 * dy };
|
||||
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.by}*4+l)*W+${s.tail}*4+${s.dx > 0 ? 0 : 3}; if(mat[i]===EMPTY) {setCell(i,SAND,20,0);added++;} }` : ''}
|
||||
${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++;
|
||||
@@ -44,40 +46,61 @@ for (const v of [4, 5, 6, 7]) {
|
||||
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.by*4,c.tail*4+3,c.by*4+3,run('SAND'));
|
||||
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));
|
||||
}
|
||||
// Corner placements are separate mouths, on both axes and in either transfer direction.
|
||||
for (const v of [4,5,6,7]) for (const vertical of [false,true]) for (const fromDiagonal of [false,true]) {
|
||||
fresh();
|
||||
const [dx,dy]=run(`SCREW_DIRS[${v}]`), sv=vertical?(dy<0?0:2):(dx>0?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<N;i++)if(mat[i]===SAND){
|
||||
const x=i%W>>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);
|
||||
// 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]) {
|
||||
@@ -129,21 +152,20 @@ function watchBars(s, n=600) {
|
||||
}
|
||||
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
|
||||
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 straight joins reach the same tails and heads too.
|
||||
// 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;
|
||||
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 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));
|
||||
// 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'));
|
||||
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));
|
||||
}
|
||||
@@ -198,27 +220,6 @@ 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);
|
||||
@@ -233,4 +234,8 @@ for (const v of [4,5,6,7]) {
|
||||
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();
|
||||
|
||||
+84
-37
@@ -97,58 +97,105 @@ with sync_playwright() as p:
|
||||
}''')
|
||||
check('sidewall stays behind the grain colour in pixels and smooth',
|
||||
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
|
||||
# Edge joins keep closed elbows; corner placements match two isolated free ends.
|
||||
pg.set_viewport_size({"width": 2100, "height": 1300})
|
||||
# Shared joins: E elbows bend inside the straight block; other placements keep collars.
|
||||
pg.set_viewport_size({"width": 2100, "height": 1500})
|
||||
ev("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();")
|
||||
ev("() => {" + (root / "tools/scenes/t62-joins.js").read_text() + "}")
|
||||
ev("() => {" + (root / "tools/scenes/joins.js").read_text() + "}")
|
||||
mouths = ev('''() => {
|
||||
SET.zoom=6; SET.lighting="off"; SET.glow="off"; resizeRender();
|
||||
SET.zoom=4; SET.lighting="off"; SET.glow="off"; resizeRender();
|
||||
bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera();
|
||||
const now=performance.now.bind(performance),frozen=now();
|
||||
performance.now=()=>frozen;
|
||||
try {
|
||||
const out={};
|
||||
const draw=()=>{SM.scale=1;drawWorld();};
|
||||
for(const look of ["pixel","smooth"]) {
|
||||
SET.look=look;draw();
|
||||
const bad=[];
|
||||
for(const j of T62_JOINTS) {
|
||||
const slope=j.v===4 || j.v===6?-1:1;
|
||||
const end=j.end*SCREW_DIRS[j.v][0];
|
||||
const dx=j.vertical?0:end,dy=j.vertical?end*slope:0;
|
||||
if(j.corner)continue;
|
||||
for(const side of [-1,1])for(const along of [-0.3,0.3]) {
|
||||
const x=j.bx*4+2+dx*(2+along)-dy*side*1.6;
|
||||
const y=j.by*4+2+dy*(2+along)+dx*side*1.6;
|
||||
const p=worldToScreenPx(x,y),c=R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data;
|
||||
if(!(c[0]>c[1]*1.15 && c[1]>c[2]*1.1))bad.push([j,x,y,Array.from(c)]);
|
||||
SET.look=look;SM.scale=1;drawWorld();
|
||||
const bad=[], copper=c=>c[0]>c[1]*1.15 && c[1]>c[2]*1.1;
|
||||
const sample=(x,y)=>{const p=worldToScreenPx(x,y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
||||
for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW)) {
|
||||
const slope=j.v===4 || j.v===6?-1:1, side=j.end?1:-1;
|
||||
const joins=diagonalScrewJoins(j.bx,j.by,j.v);
|
||||
if(!j.joined) {
|
||||
if(!(joins & (64<<j.end)) || (joins>>(j.end*3)&7))bad.push(['missing free collar',j]);
|
||||
// The middle of the free cap, safely inside its diagonal block.
|
||||
const cap=look==='pixel' ? [[1.5,2.5],[2.5,1.5]] : [[1.8,1.8]];
|
||||
const colors=cap.map(([x,y])=>sample(j.bx*4+2+side*x,j.by*4+2+side*slope*y));
|
||||
if(!colors.some(copper))bad.push(['free collar colour',j,colors]);
|
||||
continue;
|
||||
}
|
||||
const ex=(j.kind>=5?j.nx:j.bx)*4+2,ey=(j.kind>=5?j.ny:j.by)*4+2;
|
||||
const dx=j.vertical?0:side,dy=j.vertical?side*slope:0;
|
||||
// Both walls on the outgoing axis, beyond the rounded meeting point.
|
||||
for(const sign of [-1,1]) {
|
||||
const x=ex+dx*1.5-dy*sign*1.6,y=ey+dy*1.5+dx*sign*1.6,c=sample(x,y);
|
||||
if(!copper(c))bad.push(['open casing',j,x,y,c]);
|
||||
}
|
||||
// Follow both centrelines through the join: the bore must never show sky.
|
||||
const ax=j.bx*4+2-side*1.5,ay=j.by*4+2-side*slope*1.5;
|
||||
for(let t=0;t<=1;t+=0.05)for(const leg of [0,1]) {
|
||||
const x=leg?ex+dx*t*5:ax+(ex-ax)*t,y=leg?ey+dy*t*5:ay+(ey-ay)*t,c=sample(x,y);
|
||||
if(c[2]>c[0]*1.4 && c[2]>110)bad.push(['background inside tube',j,x,y,c]);
|
||||
}
|
||||
if(j.kind>=5) {
|
||||
// The unused corner of the bent straight block must become background.
|
||||
const x=j.nx*4+2+(j.vertical?side:-side)*1.8;
|
||||
const y=j.ny*4+2+(j.vertical?-side*slope:side*slope)*1.8,c=sample(x,y);
|
||||
if(copper(c) || c[2]<c[0]*1.2)bad.push(['square casing outside elbow',j,x,y,c]);
|
||||
}
|
||||
}
|
||||
const sample=(bx,by)=>{
|
||||
const colors=[];
|
||||
for(let y=0;y<4;y++)for(let x=0;x<4;x++) {
|
||||
const p=worldToScreenPx(bx*4+x+0.5,by*4+y+0.5);
|
||||
colors.push(...R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);
|
||||
}return colors.join(',');
|
||||
};
|
||||
for(const j of T62_JOINTS.filter(j=>j.corner)) {
|
||||
const [dx,dy]=SCREW_DIRS[j.v],sx=j.bx+j.end*dx,sy=j.by+j.end*dy;
|
||||
const collar=sample(j.bx,j.by),straight=sample(sx,sy);
|
||||
const joins=diagonalScrewJoins(j.bx,j.by,j.v),end=j.end*dx>0?1:0;
|
||||
if(!(joins & (64<<end)) || (joins>>(end*3)&7))bad.push(['missing collar',j]);
|
||||
removeBlock(sx,sy);draw();
|
||||
if(sample(j.bx,j.by)!==collar)bad.push(['diagonal differs from isolated end',j]);
|
||||
placeBlock(BP_SCREW,j.straight,sx,sy,true);
|
||||
removeBlock(j.bx,j.by);draw();
|
||||
if(sample(sx,sy)!==straight)bad.push(['straight differs from normal end',j]);
|
||||
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);draw();
|
||||
}
|
||||
out[look]=bad;
|
||||
}return out;
|
||||
} finally {performance.now=now;}
|
||||
}''')
|
||||
for look, bad in mouths.items():
|
||||
check(f"{look}: edge elbows stay closed and corners show two separate free ends", not bad, bad[:2])
|
||||
check(f"{look}: shared screw joins have closed elbows, free collars and no bore holes", not bad, bad[:2])
|
||||
previews = ev('''() => {
|
||||
const bad=[], now=performance.now.bind(performance),frozen=now();
|
||||
performance.now=()=>frozen;
|
||||
try {
|
||||
for(const look of ['pixel','smooth'])for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW && j.spot===2 && j.end===1 && (j.v===5&&!j.vertical || j.v===4&&j.vertical))) {
|
||||
SET.look=look;
|
||||
const side=j.end?1:-1,slope=j.v===4||j.v===6?-1:1;
|
||||
const ux=2+(j.vertical?side:-side)*1.8,uy=2+(j.vertical?-side*slope:side*slope)*1.8;
|
||||
const b=j.by*BW+j.bx,nb=j.ny*BW+j.nx;
|
||||
const plan=new Map([[b,[BP_SCREW,j.v]],[nb,[BP_SCREW,j.straight]]]);
|
||||
removeBlock(j.bx,j.by);removeBlock(j.nx,j.ny);
|
||||
const sample=(x,y)=>{const p=worldToScreenPx(j.nx*4+x,j.ny*4+y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data).join(',');};
|
||||
SM.scale=1;drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
|
||||
for(const [n,piece] of plan)ghostBlocks.set(n,piece);
|
||||
drawWorld();
|
||||
if(sample(ux,uy)!==bg || sample(2,2)===empty)bad.push(['ghost elbow silhouette',look,j]);
|
||||
ghostBlocks.clear();
|
||||
const ctx=R.ctx,canvas=document.createElement('canvas');canvas.width=canvas.height=192;
|
||||
try {
|
||||
R.ctx=canvas.getContext('2d');R.ctx.fillStyle='#ffffff';
|
||||
previewBlock(BP_SCREW,64,64,64,64,false,j.straight,j.nx,j.ny,plan);
|
||||
const alpha=(x,y)=>R.ctx.getImageData(Math.floor(64+x*16),Math.floor(64+y*16),1,1).data[3];
|
||||
if(alpha(ux,uy)!==0 || alpha(2,2)===0)bad.push(['planned preview elbow silhouette',look,j]);
|
||||
} finally {R.ctx=ctx;}
|
||||
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);placeBlock(BP_SCREW,j.straight,j.nx,j.ny,true);
|
||||
}
|
||||
} finally {performance.now=now;ghostBlocks.clear();}
|
||||
return bad;
|
||||
}''')
|
||||
check('both looks: ghosts and planned previews leave the unused elbow corner empty', not previews, previews[:2])
|
||||
tree_tags = ev('''() => {
|
||||
SET.look='smooth';SM.scale=1;
|
||||
const now=performance.now.bind(performance),frozen=now();performance.now=()=>frozen;
|
||||
try {
|
||||
const j=JOIN_CASES.find(j=>j.v===5 && j.end===1 && !j.vertical && j.spot===2 && j.code===BP_SCREW);
|
||||
const bx=j.nx+1,by=j.ny;
|
||||
const sample=()=>{const p=worldToScreenPx(bx*4,by*4);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),4*R.zp,4*R.zp).data).join(',');};
|
||||
drawWorld();const plain=sample(),bad=[];
|
||||
for(const m of [WOOD,LEAVES]) {
|
||||
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=m;
|
||||
drawWorld();if(sample()!==plain)bad.push(m);
|
||||
}
|
||||
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=0;
|
||||
return bad;
|
||||
} finally {performance.now=now;}
|
||||
}''')
|
||||
check('smooth: trees behind a straight screw cannot become elbow join bits', not tree_tags, tree_tags)
|
||||
# a browser without hardware drawing gets the pixel look and a note
|
||||
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
||||
pg2.add_init_script('localStorage.clear();')
|
||||
|
||||
Reference in New Issue
Block a user