Fog of war: underground is black until the bot has seen it (t8)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 01:37:34 -04:00
co-authored by Claude Opus 5.5
parent a730a7d73c
commit 152a2af6df
20 changed files with 222 additions and 22 deletions
+49 -3
View File
@@ -20,7 +20,7 @@ with sync_playwright() as p:
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 == 'error' else None)
pg.goto(url); pg.wait_for_timeout(300)
pg.goto(url); pg.evaluate("SET.fog = false"); pg.wait_for_timeout(300)
check('storage transactions roll back when a write throws', pg.evaluate("""async () => {
try { await DB.op('readwrite', st => { st.put('partial', 'abort-test'); throw new Error('test abort'); }); }
catch (e) { return await DB.get('abort-test') === undefined; }
@@ -669,10 +669,12 @@ with sync_playwright() as p:
dl = dl_info.value
wf = json.loads(pathlib.Path(dl.path()).read_text())
check('Export world file downloads a world file', wf.get('format') == 'deepgrain-world' and wf['meta']['W'] == ev('W'), dl.suggested_filename)
fog_want = ev('Array.from(fog)')
check('world export includes fog bytes', wf['buffers']['fog']['type'] == 'Uint8Array')
want = ev('JSON.stringify([W, H, Math.round(money), Math.round(debt), bagTotal])')
pg3 = ctx.new_page()
pg3.on('pageerror', lambda e: logs.append(f'PAGEERROR(import): {e}'))
pg3.goto(url); pg3.wait_for_timeout(300)
pg3.goto(url); pg3.evaluate('SET.fog = false'); pg3.wait_for_timeout(300)
worlds_before_import = ev('listWorlds().then(w => w.length)')
pg3.set_input_files('#importfile', dl.path())
pg3.wait_for_function('GAME.running === true', timeout=60000)
@@ -680,13 +682,57 @@ with sync_playwright() as p:
check('importing it on the title screen brings the same world back', got == want, f'{want} -> {got}')
pg3.wait_for_function("async () => (await listWorlds()).length === " + str(worlds_before_import + 1))
check('import adds an independent saved world', pg3.evaluate('GAME.worldId') != ev('GAME.worldId') and pg3.evaluate('listWorlds().then(w => w.length)') == worlds_before_import + 1)
check('world file keeps fog bytes', pg3.evaluate('Array.from(fog)') == fog_want)
pg3.close()
pg.keyboard.press('Escape'); settle(150)
# Fog has its own paused scene; existing play checks keep the whole world visible.
fb = p.chromium.launch(args=['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
fp = fb.new_page(viewport={'width': 1280, 'height': 800})
fp.on('pageerror', lambda e: logs.append(f'PAGEERROR(fog): {e}'))
fp.add_init_script('window.__allowSoftwareGL = true')
fp.goto(url)
fp.evaluate('startNewWorld(4242, 512, 384)')
fp.wait_for_function('GAME.running')
fp.evaluate("""() => {
GAME.paused = true; SET.fog = true; SET.look = 'pixel'; SET.zoom = 6; SET.lighting = 'off';
SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender();
for (let y = 130; y < 280; y++) for (let x = 160; x < 350; x++) setCell(y * W + x, STONE, 20, F_NAT);
for (let y = 140; y < 205; y++) for (let x = 240; x < 252; x++) setCell(y * W + x, EMPTY, 20, 0);
for (let y = 192; y < 205; y++) for (let x = 240; x < 280; x++) setCell(y * W + x, EMPTY, 20, 0);
bot.x = 243; bot.y = 192; fog.fill(0); fogInit(); fogLook(bot.x + 2.5, bot.y + 2.5);
snapCamera(); GAME.hidePreviews = true; $('cards').replaceChildren();
}""")
fp.wait_for_timeout(300)
sample = "(() => { drawWorld(); const k = (235 - R.vy0) * R.bw + 300 - R.vx0; return R.px[k] & 0xffffff; })()"
check('unexplored pixel is out-of-world black', fp.evaluate(sample) == 0x0a0a0c)
check('sky starts seen and draws colour', fp.evaluate("(() => { const cy=R.camY; R.camY=0; drawWorld(); const c=R.px[20*R.bw+20]&0xffffff; R.camY=cy; drawWorld(); return fogSeen(300,0)===255 && c!==0x0a0a0c; })()"))
check('unexplored canvas pixel is black', fp.evaluate("(() => {const s=worldToScreenPx(300.5,235.5);return Array.from(R.ctx.getImageData(s.x,s.y,1,1).data).slice(0,3).every(v=>v<=12)})()"))
check('hover and cursor share only Unexplored', fp.evaluate("JSON.stringify(hoverInfo(300,235)) === JSON.stringify({t1:'Unexplored',t2:[],t3:[]})"))
fp.evaluate("GAME.aim = {x:300,y:235}; renderInfo()")
check('info box says only Unexplored', fp.locator('#info').inner_text() == 'Unexplored')
check('cursor draws only Unexplored', fp.evaluate("""() => {
const old=R.ctx.fillText, inside=GAME.mouse.in, texts=[];
R.ctx.fillText=function(t) { texts.push(t); }; GAME.mouse.in=true;
drawCursorLabel(500,400,1); R.ctx.fillText=old; GAME.mouse.in=inside;
return texts.length===1 && texts[0]==='Unexplored';
}"""))
fp.screenshot(path=str(out / 'fog-pixel.png'))
fp.evaluate('R.lens = true')
check('thermal lens hides unexplored grains', fp.evaluate(sample) == 0x0a0a0c)
fp.evaluate('R.lens = false; SET.fog = false')
check('fog toggle shows the same rock', fp.evaluate(sample) != 0x0a0a0c)
fp.evaluate("SET.fog = true; SET.look = 'smooth'; SM.t0 = performance.now() + 100000")
fp.wait_for_timeout(500)
check('fog draws in the smooth look', fp.evaluate('R.smoothOn && SM.ok'))
check('unseen smooth grains have no terrain shape', fp.evaluate("(() => {const k=(235-R.vy0)*R.bw+300-R.vx0;return R.px[k]===0x040a0a0c && R.backPx[k]===0x000a0a0c && R.treePx[k]===0 && R.fieldPx[k]===255})()"))
fp.screenshot(path=str(out / 'fog-smooth.png'))
fb.close()
# 11. the largest world from the title screen
pg2 = ctx.new_page()
pg2.on('pageerror', lambda e: logs.append(f'PAGEERROR(large): {e}'))
pg2.goto(url); pg2.wait_for_timeout(300)
pg2.goto(url); pg2.evaluate('SET.fog = false'); pg2.wait_for_timeout(300)
check('the size menu offers small, medium and large, and no deep world', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')") == '2048x1024,3072x1280,4096x1536', pg2.evaluate("[...document.querySelectorAll('#nw-size option')].map(o => o.value).join(',')"))
pg2.click('#btn-new'); pg2.fill('#nw-seed', '4242'); pg2.select_option('#nw-size', '4096x1536'); pg2.click('#btn-generate')
pg2.wait_for_function('GAME.running === true', timeout=90000)
+24
View File
@@ -0,0 +1,24 @@
// Fog: surface, sight, range, saves and simulation independence.
const { load, suite, flatWorld, rect, ticks } = require('./harness');
const { run } = load(), { check, done } = suite();
run('generateWorld(4242, 512, 384, null)');
check('sky and surface start seen', run('fogSeen(20, 0) >= 200 && fogSeen(20, surfY[20]) >= 200'));
check('deep ground starts unseen', run('fogSeen(20, surfY[20] + 60) === 0'));
flatWorld(run, 256, 256, 40);
rect(run, 40, 100, 140, 110, run('EMPTY'));
rect(run, 40, 125, 140, 135, run('EMPTY'));
run('bot.x = 60; bot.y = 102; fogLook(62.5, 104.5)');
check('open pocket is visible', run('fogSeen(82, 104) >= 200'));
check('rock hides separate pocket', run('fogSeen(62, 130) === 0'));
check('one look has limited range', run('fogSeen(124, 104) === 0'));
check('fog only increases', run('(()=>{const f = fog.slice(); fogLook(90,104); return f.every((v,i)=>fog[i]>=v)})()'));
check('save/load keeps fog', run('(()=>{fogLook(bot.x+bot.w/2,bot.y+bot.h/2);const s=serializeWorld(), f=fog.slice();deserializeWorld(s.meta,s.buffers);return f.every((v,i)=>fog[i]===v)})()'));
rect(run, 200, 160, 203, 163, run('EMPTY'));
run('placeBlock(BP_WALL, RSTONE, 50, 40, true)');
check('old save rebuilds exploration', run('(()=>{const s=serializeWorld();delete s.buffers.fog;deserializeWorld(s.meta,s.buffers);return fogSeen(62,104)>=200 && fogSeen(202,162)===255 && fogSeen(20,220)===0})()'));
check('wrong length fog rebuilds exploration', run('(()=>{const s=serializeWorld();s.buffers.fog=new Uint8Array(1);deserializeWorld(s.meta,s.buffers);return fogSeen(202,162)===255})()'));
const hash = () => run('mat.reduce((h,v)=>Math.imul(h^v,16777619)>>>0,2166136261)');
run('generateWorld(42, 256, 256, null)'); ticks(run, 300); const h = hash();
run('generateWorld(42, 256, 256, null); fogLook = function () {}'); ticks(run, 300);
check('fog leaves simulation unchanged', hash() === h);
done();
+2 -2
View File
@@ -16,7 +16,7 @@ with sync_playwright() as p:
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.wait_for_timeout(300)
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
@@ -38,7 +38,7 @@ with sync_playwright() as p:
# 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.wait_for_timeout(300)
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]'))