Files
deepgrain-foundry/test/t_smooth.py
T
44r0n7andClaude Opus 5.5 3ee85429fd Sidewall belt and screen drawing, cheaper cleat checks (t42, t43)
Far sidewall drawn behind the load with cleat ribs at their physics
positions; sloped screens get a tinted mesh between rails and shake only
when loaded. Powder skips cleat checks outside chunks that have cleats,
and empty slopes let their chunks sleep. Record the screw conveyor and
stall/carry-depth decisions.

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

112 lines
7.3 KiB
Python

"""The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works."""
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
from use_venv import ensure_playwright; ensure_playwright()
from playwright.sync_api import sync_playwright
root = pathlib.Path(__file__).resolve().parent.parent
out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else root / 'shots' / 'smooth'); out.mkdir(parents=True, exist_ok=True)
fails = []
def check(name, cond, extra=''):
print(('PASS ' if cond else 'FAIL ') + name + (f' [{extra}]' if extra != '' else ''))
if not cond: fails.append(name)
with sync_playwright() as p:
b = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
pg = b.new_page(viewport={'width': 1024, 'height': 640})
logs = []
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}') if m.type in ('error', 'warning') else None)
pg.add_init_script('window.__allowSoftwareGL = true; localStorage.clear();')
pg.goto((root / 'dist' / 'index.html').as_uri()); pg.evaluate('SET.fog = false'); pg.wait_for_timeout(300)
pg.click('#btn-new'); pg.fill('#nw-seed', '777'); pg.click('#btn-generate')
pg.wait_for_function('GAME.running === true', timeout=20000); pg.wait_for_timeout(1500)
ev = pg.evaluate
check('the smooth look is the default and is drawing', ev("SET.look === 'smooth' && SM.ok && R.smoothOn"), ev('[SET.look, SM.ok, SM.why, R.smoothOn]'))
# the canvas shows the GPU image: sample a sky pixel and a ground pixel, they must differ and not be black
px = ev("""(() => { const c = R.ctx.getImageData(R.canvas.width >> 1, 4, 1, 1).data, g = worldToScreenPx(bot.x + 2, surfY[Math.floor(bot.x)] + 6), d = R.ctx.getImageData(Math.round(g.x), Math.round(g.y), 1, 1).data; return [Array.from(c), Array.from(d)]; })()""")
check('it draws sky and ground', sum(px[0][:3]) > 200 and px[0][:3] != px[1][:3], px)
pg.screenshot(path=str(out / 'smooth.png'))
ev("player.lens = true"); pg.keyboard.press('h'); pg.wait_for_timeout(1500)
check('the thermal lens draws in pixels', ev('R.lens && !R.smoothOn'))
pg.keyboard.press('h'); pg.wait_for_timeout(1500)
check('and the smooth look comes back after it', ev('!R.lens && R.smoothOn'), ev('[R.lens, R.smoothOn, SET.look, SM.ok, SM.why, SM.ms, SM.scale]'))
ev("SET.look = 'pixel'"); pg.wait_for_timeout(1500)
check('the pixel look can be chosen', ev('!R.smoothOn'))
pg.screenshot(path=str(out / 'pixel.png'))
ev("SET.look = 'smooth'"); pg.wait_for_timeout(1500)
ev("SM.scale = 0.5"); pg.wait_for_timeout(1500)
st = ev('[R.smoothOn, SM.scale, SM.canvas.width, R.canvas.width, SET.look, SM.ok, SM.why]'); check('half-resolution smooth still draws', (st[0] and st[2] == round(st[3] * st[1])) or st[1] > 0.5, st)
# Uniform colours isolate the solid/loose seam from material noise and lighting.
ground = ev('''() => {
GAME.paused = true; requestAnimationFrame = () => 0;
SET.lighting = 'off'; SET.glow = 'off'; SET.grain = 0; SET.zoom = 6;
resizeRender(); bot.x = 138; bot.y = 118; snapCamera();
for (let y = 60; y < 220; y++) for (let x = 40; x < 240; x++) {
const i = y * W + x;
setCell(i, STONE, 22, F_NAT); bgm[i] = 0; noiseB[i] = 128;
}
const fill = m => { for (let y = 100; y < 140; y++) for (let x = 100; x < 180; x++) setCell(y * W + x, m, 22); };
const at = (x, y) => { const c = R.px[(y - R.vy0) * R.bw + x - R.vx0]; return (c & 255) + (c >>> 8 & 255) + (c >>> 16 & 255); };
const out = {};
for (const lens of [false, true]) {
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); drawWorld();
const edge = at(140, 140), inside = at(140, 142), loose = at(140, 130);
fill(STONE); drawWorld();
out[lens ? 'thermal' : 'pixel'] = [edge, inside, loose, at(140, 140)];
}
R.lens = false;
const i = 140 * W + 140;
flags[i] = 0; fill(RSTONE); drawWorld(); const built = at(140, 140);
fill(STONE); drawWorld(); out.built = [built, at(140, 140)]; flags[i] = F_NAT;
fill(RSTONE); SET.look = 'smooth'; SM.scale = 1; drawWorld();
const sample = (x, y) => {
const p = worldToScreenPx(x, y);
const c = R.ctx.getImageData(Math.floor(p.x), Math.floor(p.y), 1, 1).data;
return c[0] + c[1] + c[2];
};
out.smooth = [sample(140.5, 140.08), sample(140.5, 142.5)];
const packed = [];
for (let y = 120; y < 121; y += 1 / 6) for (let x = 140; x < 141; x += 1 / 6) packed.push(sample(x, y));
out.texture = Math.max(...packed) - Math.min(...packed);
return out;
}''')
for look in ['pixel', 'thermal']:
edge, inside, loose, solid = ground[look]
check(f'{look}: ground has a darker edge against loose grains', edge < inside * 0.85 and inside == solid, ground[look])
check(f'{look}: loose rubble is lighter than solid stone', loose > inside, ground[look])
check('built solids get no loose-grain outline', ground['built'][0] == ground['built'][1], ground['built'])
check('smooth: a thin darker seam separates ground and rubble', ground['smooth'][0] < ground['smooth'][1] * 0.9, ground['smooth'])
check('smooth: packed rubble keeps pebble shading', ground['texture'] > 30, ground['texture'])
wall = pg.evaluate('''() => {
GAME.paused = true; R.lens = false; SET.fog = false;
SET.lighting = 'off'; SET.glow = 'off'; SET.grain = false;
for(let y=20;y<150;y++)for(let x=40;x<240;x++)setCell(y*W+x,EMPTY,22,0);
for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,1,25+k,25-k,true);
rebuildPower();slopeCleatsTick(false);prepareSlopeBelts(40,20,240,150);
let cell=-1;
for(let y=40;y<100 && cell<0;y++)for(let x=100;x<124;x++)
if(slopeDetailAt(y*W+x,x,y)===2){cell=y*W+x;break;}
if(cell<0)return null;
const x=cell%W,y=(cell/W)|0,control=80*W+180;
bot.x=118;bot.y=78;snapCamera();
setCell(cell,SAND,22,0);setCell(control,SAND,22,0);
noiseB[cell]=noiseB[control]=128;
const at=i=>R.px[(((i/W)|0)-R.vy0)*R.bw+i%W-R.vx0];
SET.look='pixel';drawWorld();const colors=[at(cell)&0xffffff,at(control)&0xffffff];
SET.look='smooth';drawWorld();const grainClass=at(cell)>>>24;
return {colors,grainClass};
}''')
check('sidewall stays behind the grain colour in pixels and smooth',
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
# a browser without hardware drawing gets the pixel look and a note
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
pg2.add_init_script('localStorage.clear();')
pg2.goto((root / 'dist' / 'index.html').as_uri()); pg2.evaluate('SET.fog = false'); pg2.wait_for_timeout(300)
pg2.click('#btn-new'); pg2.fill('#nw-seed', '5'); pg2.click('#btn-generate')
pg2.wait_for_function('GAME.running === true', timeout=20000); pg2.wait_for_timeout(1200)
check('software rendering falls back to pixels with a note', pg2.evaluate("SET.look === 'pixel' && !R.smoothOn && SM.why === 'software'") and pg2.locator('.toast', has_text='graphics card').count() >= 1, pg2.evaluate('[SET.look, SM.why]'))
errs = [l for l in logs if 'PAGEERROR' in l or l.startswith('error')]
check('no errors', not errs, errs[:3])
b.close()
print('FAILS:', fails)
sys.exit(1 if fails else 0)