Faces cut into loose piles collapse outward instead of draining column by column (t93)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 22:06:48 -04:00
co-authored by Claude Opus 5.5
parent 6ac5f463aa
commit 84dd8b8d0a
11 changed files with 271 additions and 9 deletions
+34
View File
@@ -0,0 +1,34 @@
// Shared Node/browser reproduction: vacuum the foot of settled 60-grain heaps, one stroke per tick.
function pileCutWorld() {
alloc(768, 600); worldSeed = 7; RS = 123;
for (let x = 0; x < W; x++) { surfY[x] = 224; dirtY[x] = 234; graniteY[x] = basaltY[x] = magmaY[x] = H; }
computeAmbient(); fillNoise();
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
const i = y * W + x; mat[i] = y >= 224 ? STONE : EMPTY;
flags[i] = y >= 224 ? F_NAT : 0; temp[i] = AMB[y];
}
bag.fill(0); bagTotal = 0; player.tankTier = 3;
resetWeather(); weather.next = 1e15; updateRoof();
}
function pileCutSetup(peak, mixed) {
const rubble = [RSTONE, CUORE, FEORE, LDIRT];
for (let y = 164; y < 224; y++) for (let x = peak - (y - 164); x <= peak + (y - 164); x++)
setCell(y * W + x, mixed ? rubble[(x + y) % rubble.length] : RSTONE, AMB[y], 0);
}
function pileCutStroke(peak) {
bot.x = peak - 50; bot.y = 170; bot.vx = bot.vy = 0;
for (let y = 223; y >= 164; y--) for (let x = peak - 100; x < peak; x++) {
if (PH[mat[y * W + x]] !== PH_POWDER) continue;
return toolVacuum(Math.min(peak - 7, x), y - 3, 6, () => true).got;
}
return 0;
}
function pileCutProfile(peak) {
let cliff = 0, previous = 0, grains = 0;
for (let x = peak - 110; x <= peak + 110; x++) {
let y = 0; while (y < 224 && PH[mat[y * W + x]] !== PH_POWDER) y++;
const height = 224 - y; cliff = Math.max(cliff, Math.abs(height - previous)); previous = height;
for (; y < 224; y++) if (PH[mat[y * W + x]] === PH_POWDER) grains++;
}
return {cliff, grains};
}
+1 -1
View File
@@ -45,7 +45,7 @@ def main():
if browser:
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
# one at a time: two headless browsers at once can starve each other and flake
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_vacuum_collapse.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
results.append(run_py(name))
bad = 0
for name, code, out, secs in results:
+59
View File
@@ -0,0 +1,59 @@
// Real nozzle cuts into settled heaps, inline and through the real Node worker pool.
const fs = require('fs');
const {createHash} = require('crypto');
const {load, suite, enableThreads, flatWorld} = require('./harness');
const {run} = load(), {check, done} = suite();
run(fs.readFileSync(require.resolve('./pile-cut.js'), 'utf8'));
const hash = () => createHash('sha256').update(run('mat')).digest('hex');
// The longer spill must check barriers even when its starting chunk has no cleats.
flatWorld(run, 96, 160, 80);
run(`for(let y=18;y<=31;y++)setCell(y*W+29,RSTONE,AMB[y],0);
setCell(29*W+30,RSTONE,AMB[29],0);setCell(30*W+30,RSTONE,AMB[30],0);
cleatMask[30*W+34]=1;cleatMask[34*W+33]=1;
powder(30*W+30,30,30,RSTONE);`);
check('face spill stops before cleats across chunk edges',run('mat[30*W+30]===EMPTY && mat[33*W+33]===RSTONE && !cleatChunks[0]'));
(async () => {
const expected = new Map();
for (const workers of [0, 2]) for (const mixed of [false, true]) for (const near of [false, true]) {
run(`pileCutWorld();pileCutSetup(160,${mixed});bot.x=700;bot.y=100;`);
const stop = workers ? await enableThreads(run, workers) : null;
try {
run('for(let k=0;k<600;k++)simTick();');
const settled = hash();
run('awake.fill(3);for(let k=0;k<60;k++)simTick();');
check('uncut heap is settled', hash() === settled);
run(`for(let k=0;k<70;k++){pileCutStroke(160);simTick();}bot.x=${near ? 110 : 700};bot.y=170;`);
const before = run('pileCutProfile(160)');
check('vacuum removes grains without losing them', run('bagTotal') > 500 && before.grains + run('bagTotal') === 3600, JSON.stringify(before));
run('for(let k=0;k<180;k++)simTick();');
const after = run('pileCutProfile(160)'), label = `${mixed ? 'mixed' : 'stone'}, ${workers} workers, bot ${near ? 'near' : 'far'}`;
check(`vacuum face slumps within 180 ticks (${label})`, after.cliff <= 2, `before ${before.cliff}, after ${after.cliff}`);
if (workers) check('helpers actually processed chunks', run('simPool.activeMax') > 0);
const key = `${mixed}/${near}`;
if (!workers) expected.set(key, hash());
else check(`vacuum collapse matches inline bytes (${label})`, hash() === expected.get(key));
run('for(let k=0;k<1200;k++){awake.fill(3);simTick();}');
const resting = hash();
run('for(let k=0;k<600;k++){awake.fill(3);simTick();}for(let k=0;k<3;k++)simTick();');
check(`settled cut heap stays put and sleeps (${label})`, hash() === resting && run('awake.every(v=>!v)'));
} finally { if (stop) await stop(); }
}
let wideExpected;
for (const workers of [0, 2]) {
flatWorld(run, 768, 600, 224);
run('bot.x=740;bot.y=100;for(let y=104;y<224;y++)for(let x=256;x<576;x++)setCell(y*W+x,RSTONE,AMB[y],0);');
const stop = workers ? await enableThreads(run, workers) : null;
try {
run('for(let k=0;k<600;k++)simTick();');
const result = run(`(()=>{let cliff=0,previous=0,grains=0;
for(let x=0;x<W;x++){let y=0;while(y<224&&mat[y*W+x]!==RSTONE)y++;
const h=224-y;cliff=Math.max(cliff,Math.abs(h-previous));previous=h;
for(;y<224;y++)if(mat[y*W+x]===RSTONE)grains++;}
return {cliff,grains};})()`);
check(`a broad 120-grain wall also releases (${workers} workers)`, result.cliff<=2 && result.grains===38400, JSON.stringify(result));
if (!workers) wideExpected=hash();
else check('broad wall release matches inline bytes',hash()===wideExpected);
} finally { if (stop) await stop(); }
}
done();
})().catch(e => { console.error(e); process.exitCode = 1; });
+58
View File
@@ -0,0 +1,58 @@
"""Vacuum cuts in the real pixel frame loop with active browser workers and hardware graphics."""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / 'tools'))
from use_venv import ensure_playwright; ensure_playwright()
from browser import launch, consume_gpu_option, capabilities
consume_gpu_option()
ROOT = pathlib.Path(__file__).resolve().parents[1]
with launch(viewport={'width': 1440, 'height': 760}) as (_, page, url):
errors = []
page.on('pageerror', lambda e: errors.append(str(e)))
page.goto(url)
caps = capabilities(page)
assert caps['isolated'] and caps['webgpu'], caps
print('GPU', caps, flush=True)
page.evaluate("""() => {
const enter = enterGame; enterGame = function() { enter(); GAME.paused = true; };
SET.workerCap=2; SET.fog=false; SET.lighting='off'; SET.cameraLead=false; SET.autosave=0;
startNewWorld(7,768,600);
}""")
page.wait_for_function('GAME.running', timeout=60000)
page.evaluate((ROOT / 'test/pile-cut.js').read_text())
page.evaluate("""() => {
pileCutWorld(); pileCutSetup(160,false); pileCutSetup(416,true);
bot.x=700;bot.y=100;for(let k=0;k<600;k++)simTick();
renderDirty.fill(1);GPU.world=null;SM.surf=-1;
}""")
page.evaluate('startThreads()')
page.evaluate("""() => {
// Tiny isolated fixtures otherwise run inline below the production dispatch threshold.
simPool.force=true;
window.cutTicks=0;window.cutEnd=0;
const tool=toolTick;toolTick=()=>{
tool();
if(cutEnd && tick===cutEnd+180) window.cutResult={tick,cutEnd,
stone:pileCutProfile(160),mixed:pileCutProfile(416),helpers:simPool.activeMax};
if(cutTicks<140){pileCutStroke(cutTicks%2?416:160);cutTicks++;if(cutTicks===140)cutEnd=tick+1;}
};
document.querySelectorAll('.card,.toast').forEach(e=>e.remove());
$('hud').hidden=true;GAME.hidePreviews=true;SET.zoom=3;resizeRender();
bot.x=288-bot.w/2;bot.y=190-bot.h/2;snapCamera();
// Keep both heaps in view while the bot follows each vacuum stroke.
updateCamera=()=>{}; GAME.paused=false;
}""")
page.wait_for_function('cutEnd>0', timeout=30000)
for delay in (0, 60, 120, 180):
page.wait_for_function(f'tick>=cutEnd+{delay}', timeout=15000)
result = page.evaluate('({tick,cutEnd,stone:pileCutProfile(160),mixed:pileCutProfile(416),helpers:simPool.activeMax})')
print('profile', delay, result, flush=True)
page.screenshot(path=str(ROOT / f'shots/t93b-time-{delay}.png'))
page.wait_for_function('window.cutResult', timeout=15000)
result = page.evaluate('cutResult')
page.evaluate('GAME.paused=true')
page.screenshot(path=str(ROOT / 'shots/t93b-after.png'))
assert result['helpers'] > 0, result
assert result['stone']['cliff'] <= 2 and result['mixed']['cliff'] <= 2, result
assert not errors, errors
print('PASS vacuum-cut stone and mixed faces slump within 180 real-loop ticks with browser helpers')