Stickiness gated every diagonal step, so clay lumps, snow, wood chunks, ash and dirt crawled down sloped walls (a clay grain never got down a 20-piece slope). It now applies only grain against grain: a grain resting on anything built always tries its diagonal step, so chutes carry every material while heaps keep their shape. The random draw is unchanged, so generated worlds stay the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
7.9 KiB
JavaScript
123 lines
7.9 KiB
JavaScript
// Sloped walls: flow, sealed joints, building, refunds and persistence.
|
|
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
|
|
const { run } = load();
|
|
const { check, done } = suite();
|
|
// Wall selection drives the same saved slope codes and sealed funnel geometry.
|
|
const fs = require('fs');
|
|
run(fs.readFileSync('src/ui/selection.js', 'utf8'));
|
|
run(`const GAME = { pieceDir: {}, drag: null }; let hudDirty = false;
|
|
function aimBlock() { return { bx: 23, by: 23 }; }`);
|
|
function fresh() { flatWorld(run, 240, 192, 176); run('AMB.fill(20);temp.fill(20);'); }
|
|
function funnel(sloped, material) {
|
|
fresh();
|
|
run(`for (const [ax,ay,bx,by] of [[12,12,23,23],[36,12,25,23]]) {
|
|
GAME.drag={bx:ax,by:ay};
|
|
const code=${sloped ? 'pieceCode(PIECE_BY_ID.wall,{bx,by})' : 'BP_WALL'};
|
|
for(const [x,y] of blockLine(ax,ay,bx,by,true)) placeBlock(code,RSTONE,x,y,true);
|
|
} GAME.drag=null;`);
|
|
rect(run, 80, 20, 119, 29, material);
|
|
ticks(run, 1800);
|
|
const out = count(run, material, 0, 96, 239, 175);
|
|
check(`${sloped ? 'slope' : 'square'} funnel conserves 400 grains`, count(run, material) === 400);
|
|
return out;
|
|
}
|
|
const sand = run('SAND'), water = run('WATER');
|
|
const slopeOut = funnel(true, sand), squareOut = funnel(false, sand);
|
|
check('all sand leaves the sloped funnel', slopeOut === 400, slopeOut);
|
|
check('square steps retain sand', squareOut < 400, squareOut);
|
|
check('all water leaves the sloped funnel', funnel(true, water) === 400);
|
|
for (const material of ['GRAVEL', 'CUORE', 'LDIRT']) check(`${material} slides down`, funnel(true, run(material)) === 400);
|
|
fresh();
|
|
run('for (let k=0;k<12;k++) placeBlock(BP_SLOPE_BL,RSTONE,12+k,12+k,true); spawnBar(BARCU,52,35);');
|
|
ticks(run, 600);
|
|
check('bars slide off the slope', run('ents.length === 1 && ents[0].iy > 96'), run('ents.map(e=>[e.ix,e.iy])'));
|
|
for (let c = 15; c <= 18; c++) {
|
|
fresh();
|
|
run(`bagAdd(RSTONE,100,true); bagAdd(BARCU,10,true);`);
|
|
const before = run('bag[RSTONE]');
|
|
run(`placeBlock(${c},RSTONE,20,20);`);
|
|
check(`slope ${c} costs half a wall`, before-run('bag[RSTONE]') === 8);
|
|
check(`slope ${c} has its triangle`, run('blockIntegrity(20*BW+20)') === 10);
|
|
run(`undoRecord('cut',()=>cutRegion(80,80,83,83)); undoLast();`);
|
|
check(`undo restores orientation ${c}`, run(`blockP[20*BW+20] === ${c}`));
|
|
check(`copy and flip preserve material and mirror ${c}`, run(`(()=>{ const bp=flipBlueprint(copyRegion(80,80,83,83)); return bp.blocks[0][2] === ${15+((c-15)^1)} && bp.blocks[0][3]===RSTONE; })()`));
|
|
run(`const snap${c}=serializeWorld(); deserializeWorld(snap${c}.meta,snap${c}.buffers);`);
|
|
check(`save restores orientation ${c}`, run(`blockP[20*BW+20] === ${c} && blockIntegrity(20*BW+20)===10`));
|
|
run(`pasteGhost(copyRegion(80,80,83,83),24,20); bot.x=96;bot.y=80;ghostTick();`);
|
|
check(`paste builds orientation ${c}`, run(`blockP[20*BW+24]===${c} && blockV[20*BW+24]===RSTONE`));
|
|
run('removeBlock(24,20);removeBlock(20,20)');
|
|
check(`slope ${c} refunds half a wall`, run('bag[RSTONE]') === before);
|
|
run(`placeBlock(${c},BARCU,20,20);removeBlock(20,20);`);
|
|
check(`metal slope ${c} refunds one bar`, run('bag[BARCU]') === 10);
|
|
}
|
|
check('wall diagonal snaps within 30 percent', run('JSON.stringify(blockLine(2,3,7,7,true))') === JSON.stringify([[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]]));
|
|
check('belts stay straight', run('blockLine(2,3,7,8).every(p=>p[1]===3)'));
|
|
check('drag chooses below or above and ignores drag reversal', run('slopeCode(5,5,0)===15 && slopeCode(-5,-5,0)===15 && slopeCode(5,-5,0)===16 && slopeCode(5,5,2)===18 && slopeCode(5,-5,2)===17'));
|
|
// Probe every open cell along both sides, including all eleven joints. A grain may
|
|
// leave an end, so measure crossing only within the interior of the diagonal run.
|
|
for (let c = 15; c <= 18; c++) for (const m of ['SAND', 'WATER']) for (const side of [-1, 1]) {
|
|
fresh();
|
|
const descending = c === 15 || c === 18;
|
|
run(`for(let k=0;k<12;k++) placeBlock(${c},RSTONE,12+k,${descending ? '12+k' : '23-k'},true);`);
|
|
const leaked = run(`(()=>{
|
|
const sideAt=(x,y)=>${descending ? 'y-x' : 'x+y-143'};
|
|
let leaks=0, probes=0;
|
|
for(let y=52;y<92;y++) for(let x=52;x<92;x++) {
|
|
const i=y*W+x, d=sideAt(x,y);
|
|
if(mat[i]!==EMPTY || Math.sign(d)!==${side} || Math.abs(d)>2) continue;
|
|
setCell(i,${m},20,0); probes++;
|
|
// Exercise the real movement functions repeatedly without letting grains escape an end.
|
|
for(let k=0;k<20 && mat[i]===${m};k++) { tickStamp++; ${m === 'WATER' ? 'liquid(i,x,y,WATER)' : 'powder(i,x,y,SAND)'}; }
|
|
for(let yy=y;yy<=y+1;yy++) for(let xx=x-32;xx<=x+32;xx++) {
|
|
if(xx<0 || xx>=W)continue;
|
|
const j=yy*W+xx;
|
|
if(mat[j]===${m}) { if(Math.sign(sideAt(xx,yy))===${-side}) leaks++; setCell(j,EMPTY,20,0); }
|
|
}
|
|
}
|
|
return [leaks,probes];
|
|
})()`);
|
|
check(`slope ${c} seals ${m} from side ${side}`, leaked[0] === 0 && leaked[1] > 0, leaked);
|
|
}
|
|
for (const c of [15, 16]) {
|
|
fresh();
|
|
run(`for(let k=0;k<12;k++) placeBlock(${c},RSTONE,12+k,${c === 15 ? '12+k' : '23-k'},true);spawnBar(BARCU,${c === 15 ? 52 : 136},35);`);
|
|
ticks(run,600);
|
|
check(`bars reach the bottom of slope ${c}`,run('ents.length===1 && ents[0].iy>96'));
|
|
}
|
|
for(const c of [15,16,17,18]) {
|
|
fresh();
|
|
check(`slope ${c} supports every wall material`,run(`WALL_LIST.every((v,k)=>{ const bx=4+k*3;bagAdd(v,16,true);const before=bag[v];if(placeBlock(${c},v,bx,20))return false;removeBlock(bx,20);return bag[v]===before; })`));
|
|
}
|
|
fresh();
|
|
run('placeBlock(BP_SLOPE_BL,RSTONE,20,20,true);placeBlock(BP_WALL,RSTONE,20,21,true);removeBlock(20,21);');
|
|
check('removing an adjoining square wall restores the slope seal',run('mat[slopeSeal(BP_SLOPE_BL,20,20)]===STONE'));
|
|
run('removeBlock(20,20)');
|
|
check('removal leaves no free seal grains behind',count(run,run('STONE'),80,80,83,87)===0);
|
|
check('old saved slope codes select Wall', run("Array.from({length:4},(_,k)=>PIECE_BY_CODE[15+k].id).every(id=>id==='wall')"));
|
|
check('single Wall starts square', run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
|
|
const shapes = [];
|
|
for (let k=0;k<5;k++) { run('turnSlope()'); shapes.push(run('pieceCode(PIECE_BY_ID.wall)')); }
|
|
check('T cycles square then four corners then square', JSON.stringify(shapes)===JSON.stringify([15,16,17,18,1]), shapes);
|
|
run('GAME.drag={bx:20,by:20}');
|
|
check('diagonal Wall defaults below', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_BL'));
|
|
run('turnSlope()');
|
|
check('T flips diagonal Wall above', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_TR'));
|
|
run('GAME.drag=null');
|
|
check('drag flip preserves single square choice',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
|
|
run('GAME.pieceDir.wall=3;GAME.drag={bx:20,by:23}');
|
|
check('straight Wall drag stays square even with a corner selected',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
|
|
run('GAME.drag=null;GAME.pieceDir.wall=1');
|
|
check('single Wall slope shows its half-block price',run('pieceCostNow(PIECE_BY_ID.wall,RSTONE)[0][1]===8'));
|
|
// Fixture saved before Wall absorbed the separate Sloped wall piece (save version 2).
|
|
const legacy = JSON.parse(require('zlib').gunzipSync(fs.readFileSync(__dirname+'/fixtures/slopes-v2.json.gz')));
|
|
globalThis.legacySlopeSave = legacy;
|
|
run('deserializeWorld(legacySlopeSave.meta,legacySlopeSave.buffers)');
|
|
check('old save loads all four slope corners unchanged',run('Array.from({length:4},(_,k)=>{const b=6*BW+2+k*3;return blockP[b]===15+k && blockV[b]===RSTONE && blockIntegrity(b)===10 && PIECE_BY_CODE[blockP[b]].id===\'wall\';}).every(Boolean)'));
|
|
delete globalThis.legacySlopeSave;
|
|
// Stickiness is grain against grain: on a built slope sticky clay lumps run as fast as sand.
|
|
const slide=m=>{flatWorld(run,256,224,208);run('for(let k=0;k<20;k++)placeBlock(BP_SLOPE_BL,RSTONE,10+k,10+k,true);');
|
|
return run(`(()=>{setCell(39*W+42,${m},20,0);for(let t=0;t<2000;t++){simTick();for(let i=0;i<N;i++)if(mat[i]===${m}&&i%W>=118)return t;}return -1;})()`);};
|
|
const sandT=slide('SAND'),clayT=slide('LCLAY');
|
|
check('clay lumps slide down a built slope as fast as sand',clayT>0 && clayT<=sandT+4,JSON.stringify([sandT,clayT]));
|
|
done();
|