Dev console: money, items, upgrades, free build, speed (t66)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -3,7 +3,7 @@
|
||||
|
||||
python3 test/run_all.py every headless suite (test/t_*.js), in parallel
|
||||
python3 test/run_all.py pipes stone only suites whose name contains one of these words
|
||||
python3 test/run_all.py --browser also the browser tests (play.py, then t_smooth.py),
|
||||
python3 test/run_all.py --browser also browser tests (play.py, t_smooth.py, t_dev_console.py),
|
||||
after rebuilding dist/; they run one at a time
|
||||
python3 test/run_all.py --browser-only
|
||||
|
||||
@@ -42,7 +42,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']: results.append(run_py(name))
|
||||
for name in ['play.py', 't_smooth.py', 't_dev_console.py']: results.append(run_py(name))
|
||||
bad = 0
|
||||
for name, code, out, secs in results:
|
||||
fails = [l for l in out.splitlines() if l.startswith('FAIL') or 'Error' in l or l.startswith('FAILS: [') and l != 'FAILS: []']
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Dev command validation, discovery, build costs, runtime controls and save metadata.
|
||||
const { load, suite, flatWorld } = require('./harness');
|
||||
const { run } = load();
|
||||
const { check, done } = suite();
|
||||
flatWorld(run, 240, 160, 110);
|
||||
const command = text => run(`devCommand(${JSON.stringify(text)})`);
|
||||
check('commands cannot change an unmarked world', command('money 123')[0].includes('warning'));
|
||||
run('enableDevConsole(); money = 100;');
|
||||
command('MoNeY -25'); command('money set 1234.5');
|
||||
check('money adds signed values and sets amounts, ignoring case', run('money === 1234.5'));
|
||||
command('money -34.5');
|
||||
check('negative money amount subtracts', run('money === 1200'));
|
||||
command('debt 500');
|
||||
check('debt sets claim amount', run('debt === 500'));
|
||||
command('debt clear');
|
||||
check('debt clear pays off claim', run('debt === 0'));
|
||||
run('seen.fill(0); EV.length = 0;');
|
||||
command('give cUoRe 7'); command('give Copper Bar 3'); command('give charc'); command('give sand 30000');
|
||||
check('give matches internal names, display names and unique prefixes', run('bag[CUORE] === 7 && bag[BARCU] === 3 && bag[CHARCOAL] === 1000'));
|
||||
check('give exceeds bag capacity, counts totals and discovers like a pickup', run("bag[SAND] === 30000 && bagTotal === 31010 && seen[SAND] && EV.some(e => e.t === 'discover' && e.m === SAND && e.how === 'pickup')"));
|
||||
check('ambiguous prefix explains candidates', command('give cop')[0].startsWith('Ambiguous material:'));
|
||||
check('unknown material is explained', command('give nonexistent')[0].startsWith('Unknown material'));
|
||||
check('all internal aliases resolve exactly', run('Object.keys(DEV_MATERIALS).length === NMAT && Object.entries(DEV_MATERIALS).every(([k, v]) => devMaterial(k) === v)'));
|
||||
command('give sand 2147483647');
|
||||
check('give rejects typed-array overflow', run('bag[SAND] === 30000'));
|
||||
command('bit 1e100'); command('bag -5');
|
||||
check('tier setters clamp to valid range', run('player.bitTier === BIT_TIERS.length - 1 && player.tankTier === 0'));
|
||||
check('tier queries list every tier', command('bit').length === run('BIT_TIERS.length') && command('bag').length === run('TANK_TIERS.length'));
|
||||
run('player.lens = player.flask = false;'); command('unlock');
|
||||
check('unlock learns materials, rewards and milestones', run('seen.every(Boolean) && player.lens && player.flask && MILESTONES.every(m => ledger[m.id])'));
|
||||
command('tp -500 9999');
|
||||
check('teleport clamps the entire bot inside the world', run('bot.x === 0 && bot.y === H - bot.h && bot.vx === 0 && bot.vy === 0'));
|
||||
run('devCommand("tp", {x: 70, y: 45})');
|
||||
check('teleport can use the cursor', run('bot.x === 70 && bot.y === 45'));
|
||||
command('speed 1e100'); check('speed clamps high', run('devSpeed === 8'));
|
||||
command('speed -1'); check('speed clamps low', run('devSpeed === 0.1'));
|
||||
command('step'); check('step requires dev pause', run('devSteps === 0'));
|
||||
command('pause'); command('step 5');
|
||||
check('paused steps are queued for the frame loop', run('devPaused && devSteps === 5'));
|
||||
command('pause'); check('resuming discards queued steps', run('!devPaused && devSteps === 0'));
|
||||
const beforeBad = run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])');
|
||||
for (const text of ['money', 'money set', 'money Infinity', 'money 1e309', 'money 1 2', 'debt -1', 'debt nope', 'give sand -1', 'give sand 1.5', 'bit 1.5', 'bag nope', 'unlock extra', 'tp 1', 'tp NaN 4', 'free maybe', 'speed NaN', 'step 0', 'step 10001', 'pause extra', 'weather snow', 'nonsense']) {
|
||||
const output = command(text);
|
||||
check('bad arguments produce a helpful error: ' + text, output.length > 0 && !output[0].startsWith('>'));
|
||||
}
|
||||
check('invalid commands do not mutate or log', beforeBad === run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])'));
|
||||
check('help lists every command', command('help').length === 13);
|
||||
run('money = 17; bag.fill(0); bagTotal = 0; bagAdd(RSTONE, 32, true); bagAdd(LCLAY, 100, true);');
|
||||
check('normal build costs are checked', run('!canAfford([[CASH, 100]], 1)'));
|
||||
command('free on');
|
||||
const buildBag = run('JSON.stringify(Array.from(bag))');
|
||||
check('free bypasses affordability', run('canAfford([[CASH, 100000], [BARCU, 99]], 1)'));
|
||||
check('free places a wall without taking existing rubble', run("placeBlock(BP_WALL, RSTONE, 10, 10, false) === '' && bag[RSTONE] === 32"));
|
||||
check('free places a machine without charging money', run("placeMachineAt('fan', 0, 24, 10) === '' && money === 17"));
|
||||
check('free never charges, including blueprint ghosts built later', run('(() => { const m = money; payCost([[CASH, 500]], 3); return money === m; })()'));
|
||||
check('free places a mold without taking clay', run("typeof addMold('ingot', 35, 10, LCLAY, false) !== 'string' && bag[LCLAY] === 100"));
|
||||
run("ghostBlocks.set(12 * BW + 17, [BP_BELT, 1]); ghostMachines.push({type: 'fan', dir: 0, bx: 20, by: 17}); ghostMolds.push({kind: 'ingot', bx: 9, by: 16, wall: LCLAY}); ghostTick(); ghostTick();");
|
||||
check('ghost blocks, machines and molds build free too', run('!ghostBlocks.size && !ghostMachines.length && !ghostMolds.length && money === 17 && ghostMissing === 0'));
|
||||
check('all free paths preserve bag contents and totals', buildBag === run('JSON.stringify(Array.from(bag))') && run('bagTotal === 132'));
|
||||
command('free off'); run('payCost([[CASH, 2], [RSTONE, 4]], 1)');
|
||||
check('normal payment resumes after free off', run('money === 15 && bag[RSTONE] === 28 && bagTotal === 128'));
|
||||
command('weather rain');
|
||||
check('weather rain starts a shower near the bot', run('weather.shower && rainAt(bot.x) > 0 && weather.shower.until === tick + 3600'));
|
||||
command('weather clear');
|
||||
check('weather clear stops current shower', run('weather.shower === null && weather.raining.every(v => !v)'));
|
||||
run('for (let i = 0; i < 210; i++) devCommand("money 1");');
|
||||
check('dev log keeps the last 200 commands', run('devLog.length === 200 && devLog.every(e => e[0] === tick && e[1] === "money 1")'));
|
||||
run('devCommand("speed 4"); devCommand("pause"); devCommand("free on"); const devSaved = serializeWorld(); const devLogCopy = JSON.stringify(devLog); deserializeWorld(devSaved.meta, devSaved.buffers);');
|
||||
check('dev mark and command log survive save round trip', run('devWorld && devSaved.meta.dev === true && JSON.stringify(devLog) === devLogCopy'));
|
||||
check('load resets speed, free building, pause and pending steps', run('devSpeed === 1 && !devFree && !devPaused && devSteps === 0'));
|
||||
run('alloc(240,160)');
|
||||
check('a new world starts unmarked without a dev log', run('!devWorld && devLog.length === 0'));
|
||||
done();
|
||||
@@ -0,0 +1,100 @@
|
||||
"""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 playwright.sync_api import sync_playwright
|
||||
|
||||
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 sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page(viewport={'width': 1280, 'height': 760})
|
||||
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((ROOT / 'dist/index.html').as_uri())
|
||||
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)
|
||||
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)
|
||||
# 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;
|
||||
}'''))
|
||||
check('no browser errors', not errors)
|
||||
browser.close()
|
||||
|
||||
sys.exit(1 if fails else 0)
|
||||
Reference in New Issue
Block a user