Sloped walls with a flat 45° face, and diagonal wall lines (t36)

- New Sloped wall piece: a half-block triangle in any wall material, at half a wall's cost.
  Dragging walls or slopes at about 45° lays a diagonal line; the drag picks the slope's
  orientation and T flips or turns it.
- Each slope fills one cell past its corner, so a diagonal run is sealed against grains and water.
- New test/t_slopes.js: a slope funnel passes 400/400 sand (square steps 379/400).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 17:17:51 -04:00
co-authored by Claude Opus 5.5
parent 1a300bf744
commit 381019b7aa
21 changed files with 311 additions and 58 deletions
+19
View File
@@ -676,6 +676,25 @@ with sync_playwright() as p:
check('bot can sit inside a built wall', not ev(f'botOverlapSolid({wx_ - 2}, {wy_ - 2})'))
check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})'))
# Slopes use the same mouse drag path as walls, with T for their corner.
pg.click('[data-piece="slope"]'); settle(60)
check('Sloped wall is next to Wall in the build menu', ev("piece().id === 'slope' && BUILD_CATS[0][1].slice(0,2).join(',') === 'wall,slope'"))
ev('GAME.wallMat = RSTONE; GAME.pieceDir.slope = 0')
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
check('diagonal slope drag places four oriented blocks', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)'))
pg.keyboard.press('Control+z'); settle(60)
pg.keyboard.press('t'); settle(60)
check('T cycles the single slope corner', ev('GAME.pieceDir.slope === 1'))
pg.mouse.move(*blk(bbx - 5, bby - 12)); settle(60); pg.mouse.down()
pg.mouse.move(*blk(bbx - 2, bby - 9)); settle(60)
pg.keyboard.press('t'); settle(60); pg.mouse.up(); settle(60)
check('T flips a diagonal slope above its face', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_TR)'))
pg.keyboard.press('Control+z'); settle(60)
pg.click('[data-piece="wall"]'); settle(60)
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
check('square wall drag can also place a diagonal', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_WALL)'))
pg.keyboard.press('Control+z'); settle(60)
# 6. ingot mold: click to place, pour, bars drop below, vacuum them
ok = select('3', "piece().id === 'ingotmold'")
check('E cycles to the ingot mold', ok)
+90
View File
@@ -0,0 +1,90 @@
// 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();
function fresh() { flatWorld(run, 240, 192, 176); run('AMB.fill(20);temp.fill(20);'); }
function funnel(sloped, material) {
fresh();
run(`for (let k=0;k<12;k++) {
placeBlock(${sloped ? 'BP_SLOPE_BL' : 'BP_WALL'}, RSTONE, 12+k, 12+k, true);
placeBlock(${sloped ? 'BP_SLOPE_BR' : 'BP_WALL'}, RSTONE, 36-k, 12+k, true);
}`);
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);
done();