"""Dev console confirmation, keyboard isolation, HUD, time controls and saved badge.""" import pathlib, sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools')) from use_venv import ensure_playwright; ensure_playwright() from browser import launch, consume_gpu_option consume_gpu_option() ROOT = pathlib.Path(__file__).resolve().parent.parent SHOTS = ROOT / 'shots' SHOTS.mkdir(exist_ok=True) fails = [] def check(name, ok): print(('PASS ' if ok else 'FAIL ') + name) if not ok: fails.append(name) with launch() as (browser, page, url): errors = [] page.on('pageerror', lambda e: errors.append(str(e))) page.add_init_script("localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));") page.goto(url) page.wait_for_function("!$('title').hidden") page.evaluate("startNewWorld(424242, 512, 384, 'Console test')") page.wait_for_function('GAME.running && !GAME.loading') page.evaluate("document.querySelectorAll('.card, .toast').forEach(e => e.remove())") page.keyboard.press('`') check('backtick opens the first-use warning', page.is_visible('#m-dialog') and not page.is_visible('#dev-console')) page.screenshot(path=str(SHOTS / 't66-warning.png')) check('warning explains permanent marking and future achievements', 'permanently' in page.inner_text('#dlg-body') and 'achievements' in page.inner_text('#dlg-body')) page.get_by_role('button', name='Cancel', exact=True).click() check('cancel leaves the world unmarked and console closed', page.evaluate("!devWorld && $('dev-console').hidden && !GAME.paused")) page.keyboard.press('`') page.get_by_role('button', name='Enable dev console', exact=True).click() page.wait_for_function("!$('dev-console').hidden && document.activeElement === $('dev-input')") check('accepting marks the world and resumes play', page.evaluate('devWorld && !GAME.paused')) check('build material chip stays stable across HUD refreshes', page.evaluate("""() => { selectTool(2); renderSelCard(); const chip = $('selcard').querySelector('[data-wall-picker]'); renderSelCard(); const stable = chip === $('selcard').querySelector('[data-wall-picker]'); selectTool(0); return stable; }""")) old_money = page.evaluate('money') page.fill('#dev-input', 'money 1000'); page.keyboard.press('Enter') page.wait_for_function(f"money === {old_money + 1000} && $('wallet').textContent.includes(fmtMoney(money))") check('money command changes the wallet HUD', True) tool = page.evaluate('GAME.tool') page.fill('#dev-input', ''); page.keyboard.type('1234wasd') check('typing game keys does not switch tools or hold movement', page.evaluate(f'GAME.tool === {tool} && keys.size === 0')) page.keyboard.press('Escape') check('Escape closes console without opening pause menu', page.evaluate("$('dev-console').hidden && $('m-pause').hidden && !GAME.paused")) page.keyboard.press('`') check('marked worlds reopen without the warning', page.evaluate("!$('dev-console').hidden && $('m-dialog').hidden")) page.keyboard.press('`') check('backtick closes even with input focused', not page.is_visible('#dev-console')) page.keyboard.press('`') def command(text): page.fill('#dev-input', text); page.keyboard.press('Enter') command('pause') tick = page.evaluate('tick') page.wait_for_timeout(150) check('dev pause freezes simulation ticks', page.evaluate('tick') == tick) position = page.evaluate("[bot.x, bot.y, bot.lamp, GAME.tool, $('bagpanel').hidden]") page.fill('#dev-input', 'give copper') page.keyboard.press('Tab') first = page.input_value('#dev-input') page.keyboard.press('Tab') check('Tab completes spaced material names and cycles matches', first.startswith('give Copper') and first != page.input_value('#dev-input') and 'Matches:' in page.inner_text('#dev-output')) page.keyboard.press('ArrowUp') check('Up recalls the newest submitted command', page.input_value('#dev-input') == 'pause') page.keyboard.press('ArrowDown') check('Down beyond newest returns an empty input', page.input_value('#dev-input') == '') page.keyboard.press('Control+l') check('Ctrl+L clears output and keeps console focus', page.evaluate("$('dev-output').children.length === 0 && document.activeElement === $('dev-input')")) page.wait_for_timeout(100) check('Tab, arrows and Ctrl+L leave bot and game controls alone', page.evaluate("[bot.x, bot.y, bot.lamp, GAME.tool, $('bagpanel').hidden]") == position and page.evaluate('keys.size === 0 && !GAME.ctrl')) command('give saw 3') check('giving a saw explains building and leaves it out of the bag', 'Saw is built from the build menu, not given.' in page.inner_text('#dev-output') and page.evaluate('bag[SAWB] === 0')) page.fill('#dev-input', 'give saw'); page.keyboard.press('Tab') check('Tab never offers the saw', page.input_value('#dev-input') == 'give saw') command('materials saw') check('material discovery omits construction pieces', 'No matching materials.' in page.inner_text('#dev-output')) command('give sand 7'); command('give copper bar 2'); command('give gate 3') check('ordinary grains and bars still arrive while gates are refused', page.evaluate('bag[SAND] === 7 && bag[BARCU] === 2 && bag[GATE] === 0')) page.screenshot(path=str(SHOTS / 't97-console.png')) command('clear') command('materials copperore') command('help give') command('give coperore') check('errors are red while results use normal colour', page.evaluate('''() => { const rows = $('dev-output').children, error = rows[rows.length - 1]; return error.classList.contains('dev-error') && getComputedStyle(error).color === 'rgb(239, 106, 85)' && getComputedStyle(error).color !== getComputedStyle(rows[0]).color; }''')) page.screenshot(path=str(SHOTS / 't94-console.png')) command('clear') check('clear command empties output', page.evaluate("$('dev-output').children.length === 0")) check('submitted history is stored across sessions', page.evaluate("JSON.parse(localStorage.getItem('deepgrain.consoleHistory')).slice(-1)[0] === 'clear'")) command('step 5') page.wait_for_function(f'tick === {tick + 5} && devSteps === 0') page.wait_for_timeout(150) check('step advances exactly five ticks then stays paused', page.evaluate('tick') == tick + 5) # t103: real bag selection and mouse input, with deterministic dev stepping into a pit. page.evaluate('''() => { player.flask = false; bag.fill(0); bagTotal = 0; flyers.length = 0; for (let y = 20; y <= 150; y++) for (let x = 60; x <= 190; x++) { const i = y * W + x; part[i] = mach[i] = bgm[i] = 0; setCell(i, y >= 145 || x === 60 || x === 190 ? STONE : EMPTY, 20, 0); } bot.x = 100; bot.y = 100; bot.vx = bot.vy = 0; SET.fog = false; SET.lighting = 'off'; SET.cameraLead = false; SET.zoom = 4; SET.cardSeconds = 0; $('cards').replaceChildren(); resizeRender(); snapCamera(); selectTool(1); GAME.blow.clear(); lastBagKey = ''; hudDirty = true; }''') command('give water 500') page.keyboard.press('Escape') page.locator('#baglist .pick[title="Select Water"]').click() check('console water is visible and selectable without the Flask', page.evaluate('!player.flask && bag[WATER] === 500 && GAME.blow.has(WATER)')) point = page.evaluate('worldToScreenPx(130, 135)') scale = page.evaluate('R.canvas.width / window.innerWidth') page.mouse.move(point['x'] / scale, point['y'] / scale) page.mouse.down(button='right') page.evaluate('runDevCommand("step 4")') page.wait_for_function('devSteps === 0') page.mouse.up(button='right') check('right click blows water without the Flask', page.evaluate('!player.flask && bag[WATER] < 500 && flyers.some(f => f.m === WATER)')) page.evaluate('runDevCommand("step 240")') page.wait_for_function('devSteps === 0') check('water lands in the pit and leaves the bag count reduced', page.evaluate('''() => { let landed = 0; for (let y = 20; y < 145; y++) for (let x = 61; x < 190; x++) if (mat[y * W + x] === WATER) landed++; return !player.flask && !flyers.length && landed > 0 && landed + bag[WATER] === 500; }''')) page.evaluate('runDevCommand("give sand 100"); hudDirty = true;') page.keyboard.down('Shift') page.wait_for_function("$('selcard').textContent.includes('Everything')") check('Shift preview includes carried water without the Flask', page.evaluate("blowAllList().includes(WATER) && [...$('baglist').querySelectorAll('.bagrow.on .nm')].some(e => e.textContent === 'Water')")) page.screenshot(path=str(SHOTS / 't103-water-pit.png')) water = page.evaluate('bag[WATER]') page.mouse.down(button='right') page.evaluate('runDevCommand("step 4")') page.wait_for_function('devSteps === 0') page.mouse.up(button='right'); page.keyboard.up('Shift') check('Shift right click blows both water and sand without the Flask', page.evaluate(f'!player.flask && bag[WATER] < {water} && bag[SAND] < 100')) # Drive the actual frame accumulator with controlled elapsed time and no scheduled frames. counts = page.evaluate('''() => { requestAnimationFrame = () => 0; GAME.paused = false; devPaused = false; SET.fpsCap = 0; SET.maxSteps = 3; const counts = []; for (const speed of [0.5, 1, 2, 8]) { devSpeed = speed; acc = 0; const before = tick; const t = performance.now(); lastT = t - 34; lastRender = t - 34; frame(t); counts.push(tick - before); } return counts; }''') check('speed scales fixed ticks and respects maxSteps', counts == [1, 2, 3, 3]) page.evaluate('devCommand("debt clear"); runDevCommand("money 1"); openNotebook("Claims")') page.wait_for_function("$('nb-body').textContent.includes('paid off')") check('cleared debt is reflected in Claims', True) page.evaluate('closeAllModals(); runDevCommand("debt 123"); openNotebook("Market")') check('new debt is reflected in the Market', '$123' in page.inner_text('#nb-body')) page.evaluate('closeAllModals()') page.evaluate("async () => { await saveWorld('main', true); await renderWorlds(); }") page.evaluate("openModal('m-worlds')") check('world list entry stores and draws the dev badge', page.locator('#worlds-list .dev-badge').is_visible() and page.inner_text('#worlds-list .dev-badge') == 'Dev console') page.screenshot(path=str(SHOTS / 't66-worlds.png')) page.evaluate('closeAllModals()') check('saving and loading preserves mark and log, resets runtime controls', page.evaluate('''async () => { const sv = await worldNewest(GAME.worldId); const count = devLog.length; await startFromSave(sv); return devWorld && devLog.length === count && devSpeed === 1 && !devPaused && !devFree && $('dev-console').hidden; }''')) page.reload() page.wait_for_function("!$('title').hidden") check('history is restored on a fresh page', page.evaluate("devInputHistory.items.slice(-1)[0] === 'give water 500' && devRecall(devInputHistory, -1) === 'give water 500'")) check('no browser errors', not errors) browser.close() sys.exit(1 if fails else 0)