Resting faces steeper than the angle of repose topple: blown sand towers collapse (t93)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
// Shared reproduction: continuously blow plain bagged sand straight above a stationary bot.
|
||||
function pileBlowSetup() {
|
||||
pileCutWorld();
|
||||
for (let x=0;x<W;x++) { surfY[x]=480; dirtY[x]=490; }
|
||||
computeAmbient();
|
||||
for(let y=0;y<H;y++)for(let x=0;x<W;x++) {
|
||||
const i=y*W+x;mat[i]=y>=480?STONE:EMPTY;flags[i]=y>=480?F_NAT:0;temp[i]=AMB[y];
|
||||
}
|
||||
bot.x=384-bot.w/2;bot.y=330-bot.h/2;bot.vx=bot.vy=0;
|
||||
bag[SAND]=40000;bagTotal=40000;updateRoof();
|
||||
}
|
||||
function pileBlowStroke() { toolBlow(383.5,250,6,SAND); }
|
||||
function pileBlowProfile() {
|
||||
let cliff=0,previous=0,grains=0,height=0;
|
||||
for(let x=0;x<W;x++) {
|
||||
let y=0;while(y<480&&mat[y*W+x]!==SAND)y++;
|
||||
const h=480-y;cliff=Math.max(cliff,Math.abs(h-previous));previous=h;height=Math.max(height,h);
|
||||
for(;y<480;y++)if(mat[y*W+x]===SAND)grains++;
|
||||
}
|
||||
return {cliff,height,grains,flying:flyers.length,bag:bagTotal,awake:awake.filter(v=>v).length};
|
||||
}
|
||||
+1
-1
@@ -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_vacuum_collapse.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_blow_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:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Owner's upward nozzle recipe, inline and with real shared-memory helpers.
|
||||
const fs = require('fs');
|
||||
const {createHash} = require('crypto');
|
||||
const {load, suite, enableThreads} = require('./harness');
|
||||
const {run} = load(), {check, done} = suite();
|
||||
for (const file of ['pile-cut', 'pile-blow']) run(fs.readFileSync(require.resolve('./'+file+'.js'), 'utf8'));
|
||||
const hash = () => {const h=createHash('sha256');for(const a of run('[mat,flags,life]'))h.update(a);return h.digest('hex');};
|
||||
// A supported grain must release immediately, without relying on gravity underneath it.
|
||||
run(`pileBlowSetup();for(let y=270;y<340;y++)for(let x=356;x<376;x++)setCell(y*W+x,SAND,20,0);
|
||||
powder(300*W+356,356,300,SAND);`);
|
||||
check('a resting tall face topples promptly',run('mat[300*W+356]===EMPTY && mat[308*W+348]===SAND && (flags[299*W+356]&F_AVALANCHE)'));
|
||||
run(`pileBlowSetup();for(let y=270;y<340;y++)for(let x=356;x<376;x++)setCell(y*W+x,SAND,20,0);
|
||||
cleatMask[300*W+352]=1;powder(300*W+356,356,300,SAND);`);
|
||||
check('resting face spill stops at a cross-chunk cleat',run('mat[308*W+353]===SAND && mat[308*W+348]===EMPTY'));
|
||||
(async () => {
|
||||
let expected;
|
||||
for (const workers of [0, 1, 2, 8]) {
|
||||
run('pileBlowSetup()');
|
||||
const stop = workers ? await enableThreads(run, workers) : null;
|
||||
try {
|
||||
run('for(let k=0;k<1200;k++){pileBlowStroke();simTick();}');
|
||||
const before = run('pileBlowProfile()');
|
||||
check(`fast upward nozzle builds a large pile (${workers} helpers)`, before.grains > 15000 && before.height > 100, JSON.stringify(before));
|
||||
run('bot.x=700;bot.y=100;for(let k=0;k<480;k++)simTick();');
|
||||
const after = run('pileBlowProfile()');
|
||||
check(`blown sand slumps within eight seconds (${workers} helpers)`, after.cliff <= 2, JSON.stringify(after));
|
||||
check('all blown sand is retained', after.grains + after.flying + after.bag === 40000);
|
||||
if (workers) {
|
||||
check('helpers actually process the blown pile', run('simPool.activeMax') > 0);
|
||||
check('blown collapse matches inline material and motion flags', hash() === expected);
|
||||
} else expected = hash();
|
||||
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 blown sand stays put and sleeps', hash() === resting && run('awake.every(v=>!v)'));
|
||||
} finally { if(stop) await stop(); }
|
||||
}
|
||||
const heights = [];
|
||||
for (const material of ['SAND','LCLAY']) {
|
||||
run(`pileBlowSetup();bag.fill(0);bagTotal=0;bot.x=700;
|
||||
for(let y=360;y<480;y++)for(let x=350;x<410;x++)setCell(y*W+x,${material},20,0);
|
||||
for(let k=0;k<1800;k++){awake.fill(3);simTick();}`);
|
||||
heights.push(run(`(()=>{let height=0;for(let x=0;x<W;x++){let y=0;
|
||||
while(y<480&&mat[y*W+x]!==${material})y++;height=Math.max(height,480-y);}return height;})()`));
|
||||
}
|
||||
check('large clay heaps remain steeper than equal sand heaps',heights[1]>heights[0],JSON.stringify(heights));
|
||||
done();
|
||||
})().catch(e => { console.error(e); process.exitCode = 1; });
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Straight-up bagged sand nozzle in the real pixel frame loop."""
|
||||
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': 900}) as (_, page, url):
|
||||
errors = []
|
||||
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||
page.goto(url)
|
||||
print('GPU', capabilities(page), flush=True)
|
||||
page.evaluate("""() => {
|
||||
const enter=enterGame;enterGame=()=>{enter();GAME.paused=true;};
|
||||
SET.workerCap=2;SET.maxSteps=1;SET.fog=false;SET.lighting='off';SET.cameraLead=false;SET.autosave=0;
|
||||
startNewWorld(7,768,600);
|
||||
}""")
|
||||
page.wait_for_function('GAME.running', timeout=60000)
|
||||
for file in ('pile-cut', 'pile-blow'):
|
||||
page.evaluate((ROOT / f'test/{file}.js').read_text())
|
||||
page.evaluate("""() => {
|
||||
pileBlowSetup();renderDirty.fill(1);GPU.world=null;SM.surf=-1;
|
||||
document.querySelectorAll('.card,.toast').forEach(e=>e.remove());
|
||||
$('hud').hidden=true;GAME.hidePreviews=true;SET.zoom=3;resizeRender();
|
||||
snapCamera();updateCamera=()=>{};
|
||||
}""")
|
||||
page.evaluate('startThreads()')
|
||||
page.evaluate("""() => {
|
||||
simPool.force=true;window.blowTicks=0;window.blowEnd=0;
|
||||
const tool=toolTick;toolTick=()=>{tool();if(blowTicks<1200){pileBlowStroke();blowTicks++;}};
|
||||
const advance=simTick;simTick=()=>{
|
||||
advance();
|
||||
if(blowTicks===1200 && !blowEnd){blowEnd=tick;window.blowBefore=pileBlowProfile();GAME.paused=true;}
|
||||
else if(blowEnd && tick===blowEnd+480){window.blowAfter=pileBlowProfile();GAME.paused=true;}
|
||||
};
|
||||
GAME.paused=false;
|
||||
}""")
|
||||
page.wait_for_function('window.blowBefore', timeout=120000)
|
||||
print('before', page.evaluate('blowBefore'), flush=True)
|
||||
page.screenshot(path=str(ROOT / 'shots/t93c-before.png'))
|
||||
page.evaluate('() => {bot.x=700;bot.y=100;GAME.paused=false;}')
|
||||
page.wait_for_function('window.blowAfter', timeout=60000)
|
||||
result=page.evaluate('({before:blowBefore,after:blowAfter,helpers:simPool.activeMax})')
|
||||
page.screenshot(path=str(ROOT / 'shots/t93c-after.png'))
|
||||
print('result', result, flush=True)
|
||||
assert result['before']['grains']>15000 and result['before']['height']>100, result
|
||||
assert result['after']['cliff']<=2 and result['helpers']>0, result
|
||||
assert sum(result['after'][k] for k in ('grains','flying','bag'))==40000, result
|
||||
assert not errors, errors
|
||||
print('PASS upward nozzle pile collapses within 480 real-loop ticks with active helpers')
|
||||
Reference in New Issue
Block a user