Files
deepgrain-foundry/test/t_slope_belts.js
T
44r0n7andClaude Opus 5.5 8ce71f478a Removing slopes no longer leaves unowned seal cells; loading clears them
A slope's seal can sit in a cell its neighbour leaves open (a slope's
open corner, or under a grate, screw or screen), but removal only
cleared it when the neighbour was a sloped wall, so cutting a sloped
belt next to a sloped filter or grate could leave a one-cell belt that
nothing owned and nothing could remove. Removal now asks whether the
neighbour fills that cell itself, and loading a world clears belt and
screen cells that no block or seal owns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:00:15 -04:00

255 lines
20 KiB
JavaScript

// Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const fs = require('fs');
const { run } = load(), { check, done } = suite();
function fresh() {
flatWorld(run, 256, 224, 208);
run('RS=7;nextPowerBuild=0;money=10000;AMB.fill(20);temp.fill(20);hot.fill(0);');
}
fresh();
check('slope belts use new codes, without renumbering old slopes', run("typeof BP_BELT_UP !== 'undefined' && BP_BELT_UP > BP_SLOPE_TR"));
function line(grade, filter = false, dir = 1) {
fresh();
if (filter) dir = grade > 0 ? 1 : 0;
const by = grade < 0 ? 32 : 16, inlet = by + (grade < 0 ? 1 : 0), outlet = by + 9 * grade + (grade > 0 ? 1 : 0);
run(`for(let k=0;k<10;k++) placeBlock(${filter ? 'BP_FILTER_UP' : 'BP_BELT_UP'}+${grade>0?1:0},${filter?dir*128:dir},16+k,${by}+k*${grade},true,{m:[SAND],inv:0});
for(let x=10;x<16;x++)placeBlock(BP_BELT,${dir},x,${inlet},true);
for(let x=26;x<32;x++)placeBlock(BP_BELT,${dir},x,${outlet},true);`);
return { by, inlet: filter && grade < 0 ? outlet : inlet, outlet, grade, left: filter && grade < 0, feedX: filter && grade < 0 ? 120 : 44 };
}
function collect(s, steps, feed = false, sink = 128) {
return run(`(()=>{let out=0,added=0;for(let k=0;k<${steps};k++){
${feed ? `for(let l=1;l<=4;l++){const i=(${s.inlet}*4-l)*W+${s.feedX || 44};if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}` : ''}
simTick();
for(let i=0;i<N;i++)if(mat[i]===SAND && ${s.left ? "i%W<40" : "i%W>="+sink}){out++;setCell(i,EMPTY,20,0);}
for(let j=flyers.length-1;j>=0;j--)if(flyers[j].m===SAND && ${s.left ? "flyers[j].x<40" : "flyers[j].x>="+sink}){out++;flyers.splice(j,1);}
}return {out,added,left:mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length};})()`);
}
const rates=[];
for(const grade of [0,-1,1]) {
let s;
if(grade) s=line(grade);
else {fresh();run('for(let bx=10;bx<32;bx++)placeBlock(BP_BELT,1,bx,24,true);');s={inlet:24};}
collect(s,220,true);
const r=collect(s,600,true);rates.push(r.out/10);
// uphill pockets carry about as much as a flat belt, and downhill runs like one
const floor = 60;
check(`grade ${grade} carries a generous feed at least ${floor} grains/s`,r.out>0 && r.out/10>=floor,JSON.stringify(r));
const end=collect(s,600);
check(`grade ${grade} leaves no load stuck`,end.left===0,JSON.stringify(end));
}
console.log('Throughput grains/s flat/up/down: '+rates.join('/'));
for(const grade of [-1,1]) {
const s=line(grade);
rect(run,40,s.inlet*4-8,55,s.inlet*4-1,run('SAND'));
rect(run,39,20,39,s.inlet*4-1,run('STONE'));
const r=collect(s,1000);
check(`grade ${grade} conserves and delivers every grain across both flat joins`,r.out===128 && r.left===0,JSON.stringify(r));
line(grade);
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
ticks(run,500);
check(`grade ${grade} carries a bar through both flat joins`,run('ents.length===1 && ents[0].ix>=128'),run('ents.map(e=>[e.ix,e.iy])'));
}
for(const grade of [-1,1]) {
const s=line(grade);
rect(run,68,s.by*4-12,71,s.by*4-5,run('WATER'));
const before=count(run,run('WATER'));
ticks(run,600);
check(`water drains through grade ${grade} and is conserved`,count(run,run('WATER'))===before && count(run,run('WATER'),0,200,255,207)===before);
// A completely full face held at its head must not unload backwards or through corners.
line(grade);
run(`for(let x=64;x<104;x++){const by=${s.by}+((x>>2)-16)*${grade},c=blockP[by*BW+(x>>2)],y=beltFaceY(c,x>>2,by,x);for(let l=1;l<=4;l++)setCell((y-l)*W+x,SAND,20,0);}`);
rect(run,104,0,104,s.outlet*4-1,run('STONE'));
ticks(run,300);
const held=run('mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length');
check(`blocked grade ${grade} conserves its load and lets overflow roll back`,held===160 && (grade>0 || count(run,run('SAND'),0,0,63,207)>0),held);
rect(run,104,0,104,s.outlet*4-1,run('EMPTY'));
const released=collect(s,800);
check(`grade ${grade} resumes after the head clears`,released.out===160 && released.left===0,JSON.stringify(released));
line(grade);
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});setCell((${s.by+9*grade}*4+${grade<0?0:3}-1)*W+103,SAND,20,0);tick=2;tickStamp++;machinesTick();`);
check(`grade ${grade} launches off an open end along its face`,run(`flyers.length===1 && flyers[0].vx>0 && Math.sign(flyers[0].vy)===${grade}`));
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
const barLaunch=run('(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=104)return ents[0].vx>0;}return false;})()');
check(`grade ${grade} keeps a bar moving forward off an open end`,barLaunch);
for(const diagonal of [false,true]) {
line(grade);
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});for(let k=0;k<3;k++)placeBlock(BP_SCREW,${diagonal?(grade<0?4:5):1},26+k,${s.by+9*grade+(grade<0?-1:0)}+k*${diagonal?grade:0},true);`);
rect(run,40,s.inlet*4-8,55,s.inlet*4-1,run('SAND'));
rect(run,39,20,39,s.inlet*4-1,run('STONE'));
const r=collect(s,1000,false,116);
check(`grade ${grade} unloads grains into a ${diagonal?'diagonal':'straight'} screw`,r.out===128 && r.left===0,JSON.stringify(r));
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
ticks(run,600);
check(`grade ${grade} unloads a bar into a ${diagonal?'diagonal':'straight'} screw`,run('ents.length===1 && ents[0].ix>=116 && !mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)'),run('ents.map(e=>[e.ix,e.iy])'));
}
}
for(const grade of [-1,1]) {
const s=line(grade,true);
run('for(const b of filterCfg.keys())setFilter(b,{m:[SAND],inv:1});');
collect(s,220,true);
const r=collect(s,600,true);
console.log(`Filter throughput grade ${grade}, grains/s: ${r.out/10}`);
check(`downhill screen grade ${grade} discharges its generous feed`,r.out/10>=45 && r.out/10<=90,JSON.stringify(r));
}
for(const grade of [-1,1]) for(const inv of [0,1]) {
const s=line(grade,true);
run(`for(const b of filterCfg.keys())setFilter(b,{m:[SAND],inv:${inv}});`);
rect(run,s.left?128:39,20,s.left?128:39,s.inlet*4-1,run('STONE'));
run(`for(let y=${s.inlet*4-8};y<${s.inlet*4};y++)for(let x=${s.left?112:40};x<${s.left?128:56};x++)setCell(y*W+x,(x+y)&1?SAND:GRAVEL,20,0);`);
const result=run(`(()=>{const out=[0,0],below=[0,0];for(let k=0;k<1200;k++){
simTick();
for(let i=0;i<N;i++)if(mat[i]===SAND || mat[i]===GRAVEL){const m=mat[i]===SAND?0:1,x=i%W,y=(i/W)|0;
if(${s.left?"x<40":"x>=128"}){out[m]++;setCell(i,EMPTY,20,0);}else if(y>=180){below[m]++;setCell(i,EMPTY,20,0);}}
for(let f=flyers.length-1;f>=0;f--){const p=flyers[f];if((p.m===SAND || p.m===GRAVEL) && ${s.left?"p.x<40":"p.x>=128"}){out[p.m===SAND?0:1]++;flyers.splice(f,1);}}
}return {out,below,left:mat.reduce((n,m)=>n+(m===SAND || m===GRAVEL),0)+flyers.filter(f=>f.m===SAND || f.m===GRAVEL).length};})()`);
console.log(`Sorting grade ${grade} inverse ${inv}: `+JSON.stringify(result));
check(`grade ${grade} ${inv?'carries selected':'drops selected'} and conserves the full mix`,result.out[inv?0:1]===64 && result.below[inv?1:0]===64 && result.left===0 && result.out[inv?1:0]===0 && result.below[inv?0:1]===0,JSON.stringify(result));
}
for(const c of ['BP_BELT_UP','BP_BELT_DOWN','BP_FILTER_UP','BP_FILTER_DOWN'])for(const d of [0,1]) {
fresh();
run(`placeBlock(${c},${c.includes('FILTER')?d*128:d},20,20,true,{m:[SAND,BARCU],inv:1});globalThis.saved=serializeWorld();deserializeWorld(saved.meta,saved.buffers);`);
check(`${c} ${d} saves its shape, direction and settings`,run(`blockP[20*BW+20]===${c} && blockV[20*BW+20]===${c.includes('FILTER')?'filterVariant('+c+','+d+'*128)':d} && blockIntegrity(20*BW+20)===10 ${c.includes('FILTER')?'&& sameFilter(filterInfo(20*BW+20),{m:[SAND,BARCU].sort((a,b)=>a-b),inv:1})':''}`));
if(c.includes('BELT')) {
run(`slopePhase[20*BW+20]=${d?1:7};rebuildPower();slopeCleatsTick(false);globalThis.savedPhase=serializeWorld();deserializeWorld(savedPhase.meta,savedPhase.buffers);`);
// only an uphill run has cleats
const up=(c==='BP_BELT_UP')===(d===1);
check(`${c} saves the moving cleat phase and immediately restores its barriers`,run(`slopePhase[20*BW+20]===${d?1:7} && cleatCells.length${up?'>0':'===0'}`));
}
run(`globalThis.clip=copyRegion(80,80,83,83);bot.x=96;bot.y=80;undoRecord('paste',()=>pasteGhost(clip,24,20));ghostTick();`);
check(`${c} ${d} copy/paste keeps all settings`,run('blockP[20*BW+24]===blockP[20*BW+20] && blockV[20*BW+24]===blockV[20*BW+20] && sameFilter(filterInfo(20*BW+20),filterInfo(20*BW+24))'));
run('undoLast();');
check(`${c} ${d} undo clears the paste including its seal`,run(`blockP[20*BW+24]===0 && blockCellList(24,20).every(i=>mat[i]===EMPTY && !part[i]) && mat[slopeSeal(${c},24,20)]===EMPTY && !part[slopeSeal(${c},24,20)]`));
run("undoRecord('cut',()=>cutRegion(80,80,83,83));undoLast();");
check(`${c} ${d} undo restores a cut`,run(`blockP[20*BW+20]===${c} && blockIntegrity(20*BW+20)===10`));
check(`${c} ${d} mirrors its shape and reverses horizontally`,run(`(()=>{const b=flipBlueprint(clip).blocks[0];return b[2]===${c}+${c.endsWith('UP')?1:-1} && b[3]===${c.includes('FILTER')?'filterVariant('+c+'+'+(c.endsWith('UP')?1:-1)+',0)':1-d};})()`));
}
for(const grade of [-1,1]) {
const s=line(grade,true);
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);ticks(run,500);
check(`sloped filter grade ${grade} slides a bar it does not pass downhill`,run(`ents.length===1 && ${s.left?'ents[0].ix<40':'ents[0].ix>=128'}`));
line(grade,true);
run(`for(const b of filterCfg.keys())setFilter(b,{m:[BARCU],inv:0});spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);ticks(run,500);
check(`sloped filter grade ${grade} drops a selected bar through its open frame`,run('ents.length===1 && ents[0].ix<104 && ents[0].iy===206 && !mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)'));
}
// Reverse the motor without flipping the upward-facing triangle.
for(const grade of [-1,1]) {
const s=line(grade,false,0);
rect(run,112,s.outlet*4-8,127,s.outlet*4-1,run('SAND'));
rect(run,128,20,128,s.outlet*4-1,run('STONE'));
const r=run(`(()=>{let out=0;for(let k=0;k<1000;k++){simTick();for(let i=0;i<N;i++)if(mat[i]===SAND && i%W<40){out++;setCell(i,EMPTY,20,0);}for(let f=flyers.length-1;f>=0;f--)if(flyers[f].m===SAND && flyers[f].x<40){out++;flyers.splice(f,1);}}return {out,left:mat.reduce((n,m)=>n+(m===SAND),0)};})()`);
check(`reversed grade ${grade} delivers every grain to the left`,r.out===128 && r.left===0,JSON.stringify(r));
line(grade,false,0);run(`spawnBar(BARCU,120,${s.outlet*4-2},0,0);`);ticks(run,500);
check(`reversed grade ${grade} carries bars left across both joins`,run('ents.length===1 && ents[0].ix<40'),run('ents.map(e=>[e.ix,e.iy])'));
}
fresh();
run('placeBlock(BP_BELT_UP,1,20,32,true);');
// The ribs hold the load: under a heavy feed no grain is ever inside a rib, and a grain on the bare
// face slides down until a rib stops it, then rides up with that rib.
{
const s=line(-1);
const inRib=run(`(()=>{let worst=0;for(let k=0;k<600;k++){for(let l=1;l<=12;l++){const i=(${s.inlet}*4-l)*W+44;if(mat[i]===EMPTY)setCell(i,SAND,20,0);}
simTick();let n=0;for(const c of cleatCells)if(PH[mat[c]]===PH_POWDER)n++;if(n>worst)worst=n;}return worst;})()`);
check('under a heavy feed no grain is ever inside a rib',inRib===0,inRib);
line(-1);
// a free face cell high on the run, halfway between two ribs
const g=run(`(()=>{for(let x=90;x<100;x++){const b=((32-((x>>2)-16))*BW)+(x>>2),y=beltFaceY(blockP[b],x>>2,32-((x>>2)-16),x);
let near=99;for(let h=0;h<CLEAT_SPACING;h++){if(cleatFoot(x-h,b)||cleatFoot(x+h,b)){near=h;break;}}
if(near>=3){setCell((y-1)*W+x,SAND,20,0);return x;}}return -1;})()`);
const path=run(`(()=>{const xs=[];for(let k=0;k<40;k++){simTick();for(let i=0;i<N;i++)if(mat[i]===SAND){xs.push(i%W);break;}}return xs;})()`);
check('a grain on the bare face slides back down to a rib',g>0 && Math.min(...path)<g,JSON.stringify([g,path.slice(0,12)]));
check('then rides up with its rib',path[path.length-1]>Math.min(...path),JSON.stringify(path.slice(-8)));
}
// A piece added to a running belt takes up its cleat spacing, so no rib starts at the seam and
// pushes its pocket into the one ahead (that stalled the whole run).
for(const wait of [3,7,11,13]){
fresh();
run(`for(let k=0;k<9;k++)placeBlock(BP_BELT_UP,1,16+k,40-k,true);for(let k=1;k<=6;k++)placeBlock(BP_BELT,1,16-k,41,true);
for(let t=0;t<${wait};t++)simTick();
for(let k=9;k<12;k++)placeBlock(BP_BELT_UP,1,16+k,40-k,true);for(let k=1;k<=6;k++)placeBlock(BP_BELT,1,27+k,29,true);`);
const r=run(`(()=>{let added=0,past=0;for(let t=0;t<1500;t++){if(t<300&&t%10===0){const i=(41*4-1)*W+50;if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}simTick();}
for(let i=0;i<N;i++)if(mat[i]===SAND&&i%W>=112)past++;return {added,past,same:slopePhase[32*BW+24]===slopePhase[31*BW+25]};})()`);
check(`a piece added ${wait} ticks later shares the run's phase and the run keeps carrying`,r.same && r.added>0 && r.past===r.added,JSON.stringify(r));
}
// A stationary cleat blocks downhill crossings; the bare face still lets grains slide.
for (const grade of [-1,1]) {
const s=line(grade);
run('rebuildPower();slopeCleatsTick(false);');
if(grade>0) check('a downhill run has no cleats',run('cleatCells.length===0'));
else check(`grade ${grade} cleats block every downhill crossing`,run(`cleatCells.length>0 && cleatCells.every(i=>!enter(SAND,PH_POWDER,i,${grade},1) && !enter(SAND,PH_POWDER,i+W+${grade},${grade},1))`));
check(`grade ${grade} bare face slides between steps without stamping the load`,run(`(()=>{
for(let x=72;x<96;x++){
const by=${s.by}+((x>>2)-16)*${grade}, y=beltFaceY(blockP[by*BW+(x>>2)],x>>2,by,x)-1,i=y*W+x;
if(cleatMask[i] || cleatMask[i+W+${grade}])continue;
setCell(i,SAND,20,0);tick=3;tickStamp++;machinesTick();
const unstamped=stamp[i]!==tickStamp;
for(let k=0;k<200 && mat[i]===SAND;k++)powder(i,x,y,SAND);
return unstamped && mat[i]===EMPTY;
} return false;
})()`));
}
fresh();
run('placeBlock(BP_FILTER_DOWN,128,20,20,true,{m:[],inv:0});setCell(79*W+80,GRAVEL,20,0);tickStamp++;tick=SCREEN_NUDGE_TICKS-1;screenNudge(80*W+80,1,null);');
check('screen waits between its configured nudges',run('mat[79*W+80]===GRAVEL && stamp[79*W+80]!==tickStamp'));
run('tick++;screenNudge(80*W+80,1,null);');
check('screen nudges just its mesh contact downhill at the configured rate',run('mat[80*W+81]===GRAVEL && mat[79*W+80]===EMPTY'));
fresh();run('placeBlock(BP_BELT_UP,1,20,32,true);');
run(fs.readFileSync('src/render/slope-belts.js','utf8'));
// The renderer prepares joins and power first; stripe pitch is measured along the rotated band.
run('prepareSlopeBelts(0,0,W,H);');
check('sloped stripes vary along the face and travel with the motor',run('(()=>{const b=32*BW+20,c=(x,y,t)=>slopeBeltColor(b,x,y,t,0,0,0);return c(81,130,0)!==c(82,129,0) && c(81,130,0)===c(82,129,4);})()'));
run(fs.readFileSync('src/ui/selection.js','utf8'));
run(fs.readFileSync('src/ui/actions.js','utf8'));
run("globalThis.GAME={pieceDir:{belt:1,filter:1},filterCfg:{m:[SAND],inv:0},wallMat:RSTONE};globalThis.hudDirty=false;globalThis.aimBlock=()=>({bx:25,by:15});globalThis.toolId=()=>'build';globalThis.playSfx=()=>{};");
for(const id of ['belt','filter'])for(const [dx,dy] of [[5,-4],[4,5],[-5,4],[-4,-5]]) {
run('GAME.drag={bx:20,by:20};');
const c=(id==='belt'?19:21)+(dx*dy>0?1:0);
check(`${id} drag ${dx},${dy} picks the slope shape and permitted travel direction`,run(`pieceCode(PIECE_BY_ID.${id},{bx:${20+dx},by:${20+dy}})===${c} && dragDirFor(PIECE_BY_ID.${id},GAME.drag,{bx:${20+dx},by:${20+dy}})===${id==='filter'?(dx*dy>0?1:0):(dx>0?1:0)}`));
}
run("globalThis.setStatus=()=>{};globalThis.catOf=()=>0;globalThis.selectTool=()=>{};globalThis.aimBlock=()=>blockOf(GAME.aim.x,GAME.aim.y);");
for(const id of ['belt','filter'])for(const [dx,dy] of [[3,-3],[3,3],[-3,3],[-3,-3]]) {
fresh();
run(`globalThis.piece=()=>PIECE_BY_ID.${id};GAME.drag={bx:20,by:20};GAME.aim={x:${(20+dx)*4},y:${(20+dy)*4}};bot.x=80;bot.y=80;primaryRelease(false);`);
const c=(id==='belt'?19:21)+(dx*dy>0?1:0), v=id==='filter'?(dx*dy>0?128:0):(dx>0?1:0);
check(`${id} release really builds the diagonal in direction ${dx},${dy}`,run(`Array.from({length:4},(_,k)=>{const b=(20+k*${Math.sign(dy)})*BW+20+k*${Math.sign(dx)};return blockP[b]===${c} && blockV[b]===${v};}).every(Boolean)`));
run('pickUnderCursor();rotate();');
check(`${id} pick and R preserve the slope and use its permitted direction`,run(`pieceCode(PIECE_BY_ID.${id})===${c} && pieceV(PIECE_BY_ID.${id})===${id==='filter'?v:1-v}`));
run('GAME.drag=aimBlock();GAME.replace=true;primaryRelease(false);GAME.replace=false;');
check(`${id} a single replacement keeps the selected direction`,run(`blockP[${20+dy}*BW+${20+dx}]===${c} && blockV[${20+dy}*BW+${20+dx}]===${id==='filter'?v:1-v}`));
}
// Middle click on the belt band you see, even where it is drawn into the empty block next door.
fresh();
const band=run(`(()=>{globalThis.R=globalThis.R||{smoothOn:false};
for(let k=0;k<4;k++)placeBlock(BP_BELT_UP,1,20+k,24-k,true);prepareSlopeBelts(0,0,W,H);
for(let y=60;y<110;y++)for(let x=70;x<100;x++){const i=y*W+x,own=(y>>2)*BW+(x>>2);
if(!blockP[own] && mat[i]===EMPTY && slopeBeltAt(i,x,y)>=0){GAME.pieceIdx=PIECE_BY_ID.wall.index;GAME.aim={x:x+0.5,y:y+0.5};pickUnderCursor();return GAME.pieceIdx===PIECE_BY_ID.belt.index;}}
return 'no band cell outside its block';})()`);
check('middle click on a sloped belt band outside its block picks the belt',band===true,band);
run(fs.readFileSync('src/ui/selection-pickers.js','utf8'));
const filterLine=line(-1,true);
run("applyFilterRun(filterRun(32*BW+16),{m:[GRAVEL],inv:1,dir:0});");
check('the settings UI updates the matching diagonal filter run',run('filterCfg.size===10 && [...filterCfg.keys()].every(b=>sameFilter(filterInfo(b),{m:[GRAVEL],inv:1}) && blockV[b]===0)'));
run('undoLast();');
check('undo restores diagonal filter settings and direction',run('filterCfg.size===10 && [...filterCfg.keys()].every(b=>sameFilter(filterInfo(b),{m:[SAND],inv:0}) && blockV[b]===0)'));
// An actual v2 fixture, before either diagonal belt or filter existed.
const legacy=JSON.parse(require('zlib').gunzipSync(fs.readFileSync(__dirname+'/fixtures/slopes-v2.json.gz')));
globalThis.legacyBeltSave=legacy;
run('deserializeWorld(legacyBeltSave.meta,legacyBeltSave.buffers)');
check('old v2 saves keep all wall slope ids and materials',run('Array.from({length:4},(_,k)=>{const b=6*BW+2+k*3;return blockP[b]===15+k && blockV[b]===RSTONE && blockIntegrity(b)===10;}).every(Boolean)'));
// A slope's seal can sit in the open corner of a neighbouring slope or under a grate or screen;
// taking both away must leave nothing behind (it used to leave an unremovable belt cell).
const strayCells="(()=>{let n=0;for(let i=0;i<N;i++){const m=mat[i];if((m===BELTL||m===BELTR||part[i]===P_FILTER||m===RSTONE)&&!blockP[((i/W|0)>>2)*BW+((i%W)>>2)]&&slopeSealOwner(i)<0)n++;}return n;})()";
for(const [first,second] of [['BP_FILTER_DOWN,128,10,12','BP_BELT_DOWN,0,10,11'],['BP_GRATE,0,10,12','BP_BELT_DOWN,0,10,11'],['BP_FILTER,0,10,12','BP_SLOPE_BL,RSTONE,10,11']]) for(const order of [0,1]) for(const built of [0,1]) {
fresh();
const [a,b]=built?[second,first]:[first,second];
run(`placeBlock(${a},true,{m:[],inv:0});placeBlock(${b},true,{m:[],inv:0});`);
run(order?'removeBlock(10,12);removeBlock(10,11);':'removeBlock(10,11);removeBlock(10,12);');
check(`removing ${first.split(',')[0]} and the ${second.split(',')[0]} seal inside it (built ${built}, removed ${order}) leaves no stray cell`,run(strayCells)===0,run(strayCells));
}
fresh();
run("setCell(40*W+43,BELTL,20,0);globalThis.straySave=serializeWorld();deserializeWorld(straySave.meta,straySave.buffers);");
check('loading a world clears a stray belt cell left by the old removal bug',run('mat[40*W+43]===EMPTY'));
done();