533 lines
40 KiB
Python
533 lines
40 KiB
Python
"""Play-test in a real browser: mouse, keyboard and a mocked gamepad drive the game; save and continue.
|
||
Screenshots go to shots/play/ (or the folder given as the first argument). Pixel look unless the
|
||
second argument is 'smooth'."""
|
||
import json, 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' / 'play'); out.mkdir(parents=True, exist_ok=True)
|
||
url = (root / 'dist' / 'index.html').as_uri()
|
||
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()
|
||
ctx = b.new_context(viewport={'width': 1280, 'height': 800})
|
||
pg = ctx.new_page()
|
||
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.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(500)
|
||
ev = pg.evaluate
|
||
look = sys.argv[2] if len(sys.argv) > 2 else 'pixel'
|
||
check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom'))
|
||
# Ctrl+W closes the tab; with a world open the browser must ask first
|
||
leave = "(() => { const e = new Event('beforeunload', { cancelable: true }); dispatchEvent(e); return e.defaultPrevented; })()"
|
||
check('leaving the page with a world open asks to confirm', ev(leave))
|
||
check('but not on the title screen', not ev(f'(() => {{ GAME.running = false; const r = {leave}; GAME.running = true; return r; }})()'))
|
||
ev("SET.zoom = 4; resizeRender(); R.snapNext = true;") # the rest of this play-through was laid out at zoom 4
|
||
ev(f"SET.look = '{look}'"); pg.wait_for_timeout(200)
|
||
check(f'the {look} look is drawing', ev('R.smoothOn') == (look == 'smooth'), ev('[SET.look, R.smoothOn, SM.ok]'))
|
||
def cell_to_screen(x, y):
|
||
return ev(f'(() => {{ const dpr = R.canvas.width / innerWidth; const s = worldToScreenPx({x}+0.5, {y}+0.5); return [s.x/dpr, s.y/dpr]; }})()')
|
||
def blk(bx, by): return cell_to_screen(bx * 4 + 2, by * 4 + 2)
|
||
def hold(x, y, ms, button='left'):
|
||
pg.mouse.move(x, y); pg.mouse.down(button=button); pg.wait_for_timeout(ms); pg.mouse.up(button=button)
|
||
def select(key, cond, step='e', n=16):
|
||
pg.keyboard.press(key); pg.wait_for_timeout(60)
|
||
for _ in range(n):
|
||
if ev(cond): return True
|
||
pg.keyboard.press(step); pg.wait_for_timeout(30)
|
||
return ev(cond)
|
||
def settle(ms=300): pg.wait_for_timeout(ms)
|
||
def drag(fa, fc, steps=6, shift=False, hold_key=None):
|
||
# move to the start, let the camera settle, then drag using fresh screen positions
|
||
pg.mouse.move(*fa()); pg.wait_for_timeout(900)
|
||
a = fa(); pg.mouse.move(*a); pg.wait_for_timeout(50); c = fc()
|
||
if shift: pg.keyboard.down('Shift')
|
||
if hold_key: pg.keyboard.down(hold_key); pg.wait_for_timeout(40)
|
||
pg.mouse.down(); pg.mouse.move(*c, steps=steps); pg.mouse.up()
|
||
if hold_key: pg.keyboard.up(hold_key)
|
||
if shift: pg.keyboard.up('Shift')
|
||
pg.wait_for_timeout(100)
|
||
def refill(js): ev(js + '; bagTotal = 0; for (let k = 0; k < NMAT; k++) bagTotal += bag[k]; hudDirty = true;')
|
||
ev('player.tankTier = 3')
|
||
# no trees right around the work area: a test fire that spreads into one would block the belt row
|
||
ev('(() => { const x0 = Math.floor(bot.x) - 70, x1 = Math.floor(bot.x) + 30; for (let y = 0; y < surfY[Math.floor(bot.x)] + 4; y++) for (let x = x0; x < x1; x++) bgm[y * W + x] = 0; findTreeBases(); })()')
|
||
bx, by = ev('[Math.floor(bot.x+2), Math.floor(bot.y+2)]')
|
||
gy = ev(f'surfY[{bx}]')
|
||
|
||
# 1. drill: rock breaks loose, nothing enters the bag
|
||
pg.mouse.move(*cell_to_screen(bx + 6, gy + 3)); settle(900)
|
||
sx, sy = cell_to_screen(bx + 6, gy + 3)
|
||
hold(sx, sy, 3500)
|
||
settle(700)
|
||
check('drill digs with the mouse', ev('stats.dug') > 20, ev('stats.dug'))
|
||
check('drill does not fill the bag', ev('bagTotal') == 0, ev('bagTotal'))
|
||
# the resonance drill's controls: Q/E pulse width (medium to start), R tunes to what is under the cursor
|
||
check('the drill starts on a medium pulse', ev('GAME.drillW') == 1, ev('GAME.drillW'))
|
||
pg.keyboard.press('e'); settle(60)
|
||
w_wide = ev('GAME.drillW')
|
||
pg.keyboard.press('q'); pg.keyboard.press('q'); pg.keyboard.press('q'); settle(60)
|
||
check('E widens the pulse and Q narrows it, stopping at the ends', w_wide == 2 and ev('GAME.drillW') == 0, f"{w_wide} then {ev('GAME.drillW')}")
|
||
check('the drill card names the pulse width', 'narrow pulse' in pg.inner_text('#selcard'), pg.inner_text('#selcard')[:80])
|
||
pg.keyboard.press('e'); settle(60)
|
||
pg.mouse.move(*cell_to_screen(bx + 6, gy + 8)); settle(200)
|
||
pg.keyboard.press('r'); settle(60)
|
||
tuned = ev('GAME.drillTune')
|
||
check('R tunes the drill to the rock under the cursor', tuned != 0, ev('GAME.drillTune && MD[GAME.drillTune].name'))
|
||
pg.keyboard.press('r'); settle(60)
|
||
check('and R again untunes it', ev('GAME.drillTune') == 0)
|
||
check('dig milestone', ev('!!ledger.dig'))
|
||
check('discovery cards appear for new materials', pg.locator('#cards .card-pop').count() > 0, pg.locator('#cards .card-pop').count())
|
||
check('no fixed tutorial panel', pg.locator('#objective').count() == 0)
|
||
pg.screenshot(path=str(out / '01-dug.png'))
|
||
check('trees live in the background layer', ev('bgm.some(v => v === WOOD)') and ev('(() => { for (let i = 0; i < N; i++) if (mat[i] === LEAVES) return false; return true; })()'))
|
||
|
||
# 2. vacuum, left button: only the material you start on
|
||
pg.keyboard.press('2'); settle(100)
|
||
tgt = ev(f'''(() => {{ for (let r = 0; r < 30; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {{
|
||
const x = {bx + 6} + dx, y = {gy + 3} + dy, m = mat[y*W+x]; if (PH[m] === PH_POWDER && inReach(x, y)) return [x, y, m]; }} return null; }})()''')
|
||
check('loose material lies around the hole', tgt is not None)
|
||
pg.mouse.move(*cell_to_screen(tgt[0], tgt[1])); settle(900)
|
||
vx, vy = cell_to_screen(tgt[0], tgt[1])
|
||
hold(vx, vy, 1500)
|
||
settle(200)
|
||
got = ev(f'bag[{tgt[2]}]')
|
||
others = ev(f'bagTotal - bag[{tgt[2]}]')
|
||
check('vacuum (left) picks the clicked material', got > 0, f"{ev(f'MD[{tgt[2]}].name')} x{got}")
|
||
check('vacuum (left) leaves other materials', others == 0, others)
|
||
# R ticks what is under the cursor; ticked materials come along with any left-click suck
|
||
tgt2 = ev(f'''(() => {{ for (let r = 0; r < 40; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {{
|
||
const x = {bx + 6} + dx, y = {gy + 3} + dy, m = mat[y*W+x]; if (PH[m] === PH_POWDER && inReach(x, y)) return [x, y, m]; }} return null; }})()''')
|
||
if tgt2:
|
||
cx, cy = cell_to_screen(tgt2[0], tgt2[1])
|
||
pg.mouse.move(cx, cy); settle(80); pg.keyboard.press('r'); settle(80)
|
||
check('R ticks the material under the cursor', ev(f'GAME.collect.has({tgt2[2]})'), ev('[...GAME.collect]'))
|
||
ev('renderBag()'); settle(50)
|
||
ticks = pg.locator('#baglist .tick.on')
|
||
if ticks.count(): ticks.first.click(); settle(80)
|
||
check('bag tick unticks it', not ev(f'GAME.collect.has({tgt2[2]})'), ev('[...GAME.collect]'))
|
||
pg.screenshot(path=str(out / '02-vacuum.png'))
|
||
|
||
# 2b. the vacuum filter is fixed when you click: ore with stone around it, ore ticked
|
||
def mixed_pile(x0, y0):
|
||
ev(f"{{ const r = mulberry32(3); for (let y = {y0}; y < {y0} + 6; y++) for (let x = {x0}; x < {x0} + 14; x++) setCell(y*W+x, r() < 0.5 ? CUORE : RSTONE, AMB[y], 0); }}")
|
||
def suck_at(x, y, ms=900, shift=False, fly=None):
|
||
pg.mouse.move(*cell_to_screen(x, y)); settle(900)
|
||
if fly: pg.keyboard.down(fly)
|
||
if shift: pg.keyboard.down('Shift')
|
||
settle(40)
|
||
pg.mouse.move(*cell_to_screen(x, y)); pg.wait_for_timeout(40)
|
||
if ev(f'grainAt({x}, {y})') != ev(f'grainAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y))'): pg.mouse.move(*cell_to_screen(x, y)); pg.wait_for_timeout(20)
|
||
pg.mouse.down(); pg.wait_for_timeout(ms); pg.mouse.up()
|
||
if shift: pg.keyboard.up('Shift')
|
||
if fly: pg.keyboard.up(fly)
|
||
settle(100)
|
||
ev('bot.vx = bot.vy = 0;')
|
||
px0, py0 = bx - 7, gy - 12
|
||
def find_ore():
|
||
return ev(f'(() => {{ for (let y = {py0} - 8; y < {gy} + 4; y++) for (let x = {px0} - 6; x < {px0} + 20; x++) if (mat[y*W+x] === CUORE && mat[(y-1)*W+x] === EMPTY && inReach(x, y)) return [x, y]; return null; }})()')
|
||
mixed_pile(px0, py0); settle(400)
|
||
ore = find_ore()
|
||
refill('bag.fill(0)')
|
||
ev('GAME.collect = new Set([CUORE]); lastBagKey = "";')
|
||
suck_at(*ore)
|
||
check('with ore ticked, clicking ore never pulls in stone', ev('bag[CUORE]') > 0 and ev('bag[RSTONE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||
st = ev(f'(() => {{ for (let y = {py0} - 8; y < {gy} + 4; y++) for (let x = {px0} - 6; x < {px0} + 20; x++) if (mat[y*W+x] === RSTONE && mat[(y-1)*W+x] === EMPTY && inReach(x, y)) return [x, y]; return null; }})()')
|
||
suck_at(*st)
|
||
check('with ore ticked, starting on stone rubble still takes only the ore', ev('bag[CUORE]') > 0 and ev('bag[RSTONE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||
ev('GAME.collect = new Set(); lastBagKey = "";')
|
||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||
ore = find_ore()
|
||
suck_at(*ore, ms=500, shift=True, fly='d')
|
||
check('Shift held while flying boosts instead of taking everything', ev('bag[RSTONE]') == 0 or ev('bag[CUORE]') == 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||
ev(f'bot.x = {bx} - 2; bot.y = {by} - 2; bot.vx = bot.vy = 0;'); settle(300)
|
||
mixed_pile(px0, py0); settle(400); refill('bag.fill(0)')
|
||
ore = find_ore()
|
||
suck_at(*ore, ms=500, shift=True)
|
||
check('Shift held standing still takes everything', ev('bag[RSTONE]') > 0 and ev('bag[CUORE]') > 0, f"ore {ev('bag[CUORE]')}, stone {ev('bag[RSTONE]')}")
|
||
# zoom jumps straight to the bot
|
||
pg.mouse.move(640, 400); settle(600)
|
||
pg.mouse.wheel(0, -100); pg.wait_for_timeout(60)
|
||
c1 = ev('[R.camX, R.camY]'); settle(900); c2 = ev('[R.camX, R.camY]')
|
||
check('zooming lands on the bot without a pan afterwards', abs(c1[0] - c2[0]) < 1.5 and abs(c1[1] - c2[1]) < 1.5, f'{c1} -> {c2}')
|
||
pg.mouse.wheel(0, 100); settle(300)
|
||
refill('bag.fill(0)')
|
||
|
||
# 3. right button blows the chosen material out of the bot; the choice is sticky
|
||
refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120')
|
||
ok = select('2', 'GAME.dropMat === COAL && !GAME.blowMix')
|
||
check('Q/E chooses coal to blow', ok, ev('GAME.dropMat'))
|
||
dx_, dy_ = cell_to_screen(bx - 14, gy - 6)
|
||
c0 = ev('bag[COAL]')
|
||
pg.mouse.move(dx_, dy_); pg.mouse.down(button='right'); settle(150)
|
||
check('blown grains fly out of the bot', ev('flyers.length') > 0, ev('flyers.length'))
|
||
pg.screenshot(path=str(out / '03a-blowing.png'))
|
||
settle(350); pg.mouse.up(button='right'); settle(800)
|
||
check('right click blows coal out of the bag onto the ground', ev('bag[COAL]') < c0 and ev('flyers.length') == 0, f"{c0}->{ev('bag[COAL]')}")
|
||
refill('bag[COAL] = 3')
|
||
hold(dx_, dy_, 400, 'right'); settle(400)
|
||
check('chosen material stays chosen after it runs out', ev('bag[COAL] === 0 && GAME.dropMat === COAL'), ev('[bag[COAL], GAME.dropMat]'))
|
||
# tick coal and ore: Shift no longer changes what is blown, the selection does
|
||
refill('bag[COAL] = 60; bag[CUORE] = 60')
|
||
ev('GAME.collect = new Set([COAL, CUORE]); GAME.dropMat = COAL; GAME.blowMix = false; lastBagKey = ""; hudDirty = true;')
|
||
mx_, my_ = cell_to_screen(bx - 26, gy - 6)
|
||
pg.mouse.move(mx_, my_); pg.keyboard.down('Shift'); pg.mouse.down(button='right'); settle(400); pg.mouse.up(button='right'); pg.keyboard.up('Shift'); settle(600)
|
||
check('Shift + right click still blows only the chosen coal', ev('bag[CUORE]') == 60 and ev('bag[COAL]') < 60, ev('[bag[COAL], bag[CUORE]]'))
|
||
refill('bag[COAL] = 60; bag[CUORE] = 60'); settle(200)
|
||
pg.click('#baglist .mixrow .pick'); settle(200)
|
||
check('the bag offers the ticked mix as a choice', ev('GAME.blowMix') and 'Ticked mix' in pg.inner_text('#selcard'), pg.inner_text('#selcard')[:120])
|
||
pg.mouse.move(mx_, my_); pg.mouse.down(button='right'); settle(500); pg.mouse.up(button='right'); settle(600)
|
||
dc, do_ = 60 - ev('bag[COAL]'), 60 - ev('bag[CUORE]')
|
||
check('choosing Ticked mix, right click blows an even ore/coal mix', dc > 5 and abs(dc - do_) <= 2, f'coal {dc}, ore {do_}')
|
||
ev('GAME.blowMix = false;')
|
||
ev('GAME.collect = new Set(); lastBagKey = "";')
|
||
refill('bag[COAL] = 120')
|
||
|
||
# 4. igniter lights it
|
||
pg.keyboard.press('4')
|
||
spot = ev(f'(() => {{ for (let r = 0; r < 20; r++) for (const s of [1, -1]) {{ const x = {bx-14} + r * s; for (let y = 1; y < H; y++) {{ const m = mat[y*W+x]; if (m === EMPTY) continue; if (m === COAL && mat[(y-1)*W+x] === EMPTY && inReach(x, y)) return [x, y]; break; }} }} return [{bx-14}, 0]; }})()')
|
||
pg.mouse.move(*cell_to_screen(*spot)); settle(900)
|
||
ix, iy = cell_to_screen(*spot)
|
||
hold(ix, iy, 2000)
|
||
settle(1500)
|
||
check('igniter lights coal', ev('stats.ignitions') > 0, ev('stats.ignitions'))
|
||
check('thermal lens unlocked', ev('player.lens'))
|
||
pg.keyboard.press('h'); settle(200)
|
||
check('lens toggles', ev('R.lens'))
|
||
check('the lens is named on screen and framed', 'Thermal lens' in ev("$('ovbadges').textContent") and 'lens' in ev("$('ovframe').className"), pg.inner_text('#ovbadges'))
|
||
pg.screenshot(path=str(out / '03-fire-lens.png'))
|
||
pg.keyboard.press('h'); settle(150)
|
||
check('and the label and frame go when it is off', pg.inner_text('#ovbadges').strip() == '' and ev("$('ovframe').className") == '', ev("$('ovframe').className"))
|
||
|
||
# 5. build: a line of stone wall blocks by dragging, a rectangle with Shift
|
||
ok = select('3', "piece().id === 'wall'")
|
||
ev('GAME.wallMat = RSTONE; hudDirty = true')
|
||
check('build tool starts on wall blocks', ok)
|
||
settle(150)
|
||
check('the build tool shows a compact top-left card', pg.is_visible('#selcard') and pg.inner_text('#selcard .name') == ev('piece().name') and ev("costText(pieceCostNow(piece()), 1) || 'free'") in pg.inner_text('#selcard') and ev('piece().desc') not in pg.inner_text('#selcard'))
|
||
check('the build menu has no status row', pg.locator('#buildbar .bb-status').count() == 0)
|
||
check('the build menu has no short description row', pg.locator('.bb-tip').count() == 0)
|
||
for id in ['wall', 'grate']:
|
||
pg.hover(f'#buildbar [data-piece="{id}"]'); settle(120)
|
||
check(f'hover shows the full {id} description and name', pg.is_visible('#bb-pop') and pg.inner_text('#bb-pop .bb-title') == ev(f'PIECE_BY_ID.{id}.name') and pg.inner_text('#bb-pop .bb-desc') == ev(f'PIECE_BY_ID.{id}.desc'))
|
||
pg.mouse.move(400, 250); settle(120)
|
||
check('leaving the build bar hides the popup', not pg.is_visible('#bb-pop'))
|
||
ev('GAME.usingPad = true; renderBuildBar()')
|
||
check('gamepad shows the selected piece popup', pg.is_visible('#bb-pop') and pg.inner_text('#bb-pop .bb-title') == ev('piece().name'))
|
||
ev('GAME.usingPad = false; renderBuildBar()')
|
||
for key in ['1', '2']:
|
||
pg.keyboard.press(key); settle(150)
|
||
check(f'tool {key} restores the top-left card', pg.is_visible('#selcard'))
|
||
pg.keyboard.press('3'); settle(150)
|
||
pg.mouse.click(*cell_to_screen(bx, gy - 20), button='middle'); settle(120)
|
||
check('empty middle-click pick shows its status in the top-left card', 'Nothing built here to pick.' in pg.inner_text('#selcard'))
|
||
c0 = ev('GAME.buildCat != null ? GAME.buildCat : catOf(piece().id)')
|
||
pg.keyboard.press('Shift+E')
|
||
check('Shift+E switches build tabs and selects a piece in the tab', ev('GAME.buildCat') == (c0 + 1) % ev('BUILD_CATS.length') and ev('BUILD_CATS[GAME.buildCat][1].includes(piece().id)'))
|
||
pg.keyboard.press('Shift+Q')
|
||
check('Shift+Q returns to the tab and restores its wall piece', ev('GAME.buildCat') == c0 and ev("piece().id === 'wall'"))
|
||
ev('selectBuildCat(0)')
|
||
pg.keyboard.press('Shift+Q')
|
||
check('Shift+Q wraps from the first build tab to the last', ev('GAME.buildCat === BUILD_CATS.length - 1'))
|
||
pg.keyboard.press('Shift+E')
|
||
check('Shift+E wraps back to the first build tab', ev('GAME.buildCat === 0'))
|
||
p0 = ev('piece().id')
|
||
pg.keyboard.press('e')
|
||
check('plain E cycles pieces without switching tabs', ev('piece().id') != p0 and ev('GAME.buildCat') == 0)
|
||
pg.keyboard.press('q')
|
||
check('plain Q returns to the piece without switching tabs', ev('piece().id') == p0 and ev('GAME.buildCat') == 0)
|
||
pg.mouse.move(*cell_to_screen(bx + 6, gy + 8)); settle(100)
|
||
ev('window._pickN = 0; window._pickOrig = pickUnderCursor; pickUnderCursor = function () { _pickN++; }; 0') # 0: evaluate would call a returned function
|
||
pg.keyboard.press('g'); settle(60)
|
||
g_picks = ev('_pickN')
|
||
pg.mouse.click(*cell_to_screen(bx + 6, gy + 8), button='middle'); settle(60)
|
||
check('G no longer picks; middle mouse still does', g_picks == 0 and ev('_pickN') == 1, f"G {g_picks}, then {ev('_pickN')}")
|
||
ev('pickUnderCursor = _pickOrig')
|
||
check('G leaves the build piece unchanged over the world', ev('piece().id') == p0)
|
||
ev("GAME.pieceIdx = PIECE_BY_ID.wall.index; GAME.wallMat = RSTONE; hudDirty = true")
|
||
bbx, bby = ev('[Math.floor((bot.x + 2) / 4), Math.floor((bot.y + 2) / 4)]')
|
||
rb = ev('bag[RSTONE]'); nb0 = ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)')
|
||
drag(lambda: blk(bbx + 3, bby - 4), lambda: blk(bbx + 8, bby - 4))
|
||
nb1 = ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)')
|
||
check('drag places a straight line of 6 wall blocks', nb1 - nb0 == 6, f'{nb1 - nb0} blocks, {ev("GAME.status")}')
|
||
check('each wall block costs 16 rubble', rb - ev('bag[RSTONE]') == 96, rb - ev('bag[RSTONE]'))
|
||
drag(lambda: blk(bbx - 10, bby - 8), lambda: blk(bbx - 8, bby - 6), 4, True)
|
||
nb2 = ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)')
|
||
check('Shift-drag fills a 3×3 rectangle', nb2 - nb1 == 9, nb2 - nb1)
|
||
r0 = ev('bag[RSTONE]')
|
||
pg.mouse.move(*blk(bbx + 3, bby - 4)); pg.mouse.down(button='right'); settle(120); pg.mouse.up(button='right'); settle(80)
|
||
check('right click takes a block back with a refund', ev('bag[RSTONE]') - r0 == 16 and ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)') == nb2 - 1, ev('bag[RSTONE]') - r0)
|
||
pg.keyboard.press('r'); settle(60)
|
||
check('R cycles the wall material', ev('GAME.wallMat') != ev('RSTONE'), ev('MD[GAME.wallMat].name'))
|
||
ev('GAME.wallMat = RSTONE')
|
||
pg.mouse.move(*blk(bbx + 5, bby - 12)); settle(150)
|
||
pg.screenshot(path=str(out / '04-blocks.png'))
|
||
# the bot flies through built blocks but not natural rock
|
||
wx_, wy_ = ev(f'[{(bbx + 5) * 4 + 1}, {(bby - 4) * 4 + 1}]')
|
||
check('bot can sit inside a built wall', not ev(f'botOverlapSolid({wx_ - 2}, {wy_ - 2})'))
|
||
check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})'))
|
||
|
||
# 6. ingot mold: click to place, pour, bars drop below, vacuum them
|
||
ok = select('3', "piece().id === 'ingotmold'")
|
||
check('E cycles to the ingot mold', ok)
|
||
spot = ev(f'''(() => {{ for (let dx = -2; dx <= 10; dx++) for (let dy = 6; dy < 14; dy++) {{ const ox = {bbx} + dx, oy = {bby} - dy; if (!canPlaceMold('ingot', ox - 1, oy) && [0, 1, 2].every(k => entFreeAt((ox - 1) * 4 + 1 + k * 4, oy * 4 + MOLDS.ingot.h)) && inReach(ox * 4 + 8, oy * 4 + 2)) return [ox, oy]; }} return null; }})()''')
|
||
check('found a spot for the mold', spot is not None)
|
||
mx, my = blk(spot[0], spot[1])
|
||
pg.mouse.move(mx, my); settle(100); pg.mouse.down(); pg.mouse.up(); settle(100)
|
||
check('mold placed by click', ev('molds.length') == 1, ev('GAME.status'))
|
||
ev('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)')
|
||
pg.wait_for_function("ents.filter(e => e.m === BARCU).length === 3", timeout=30000)
|
||
check('bars set and drop on their own', ev("ents.length === 3 && molds[0].state === 'short' && molds[0].ready.length === 0"), ev('[molds[0].state, ents.length]'))
|
||
pg.screenshot(path=str(out / '05-mold-ready.png'))
|
||
check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])'))
|
||
settle(1500)
|
||
pg.screenshot(path=str(out / '06-bars.png'))
|
||
pg.keyboard.press('2'); settle(60)
|
||
e0 = ev('[ents[0].ix + 2, ents[0].iy + 1]')
|
||
pg.mouse.move(*cell_to_screen(*e0)); settle(900)
|
||
hold(*cell_to_screen(*e0), 900)
|
||
check('vacuum picks up bars', ev('bag[BARCU]') >= 1, ev('bag[BARCU]'))
|
||
|
||
# 7. belt line by dragging, copied with a blueprint and pasted
|
||
refill('bag[BARCU] = 60'); ev('money = 1000')
|
||
ok = select('3', "piece().id === 'belt'")
|
||
check('select belt', ok)
|
||
# keep the belt in open air, on a row clear of the dock and other machines
|
||
shift_ = ev(f'''(() => {{ for (const dx of [0, -2, -4, -6, -8, 2]) {{ let ok = true; for (let bx = {bbx} + dx - 6; bx <= {bbx} + dx + 2 && ok; bx++) {{ const c = {bby} * 4 * W + bx * 4; if (blockP[{bby} * BW + bx] || mach[c] || mach[c + 3] || mach[c + 3 * W] || !inReach(bx * 4 + 2, {bby} * 4 + 2)) ok = false; for (let y = {bby}*4 - 4; y < {bby}*4 + 8 && ok; y++) for (let x = bx*4; x < bx*4 + 4; x++) if (mach[y*W+x]) ok = false; }} if (ok) return dx; }} return 0; }})()''')
|
||
bbx += shift_
|
||
ev(f"for (let y = {bby*4 - 4}; y < {bby*4 + 8}; y++) for (let x = {(bbx-8)*4}; x < {(bbx+4)*4}; x++) if (!blockP[((y/4)|0)*BW + ((x/4)|0)]) setCell(y*W+x, EMPTY, 20, 0); for (const e of ents.slice()) if (e.ix > {(bbx-9)*4} && e.ix < {(bbx+4)*4} && e.iy > {bby*4 - 8} && e.iy < {bby*4 + 10}) removeEnt(e);")
|
||
drag(lambda: blk(bbx - 6, bby), lambda: blk(bbx + 2, bby))
|
||
nbelt = ev('blockP.reduce((a, v) => a + (v === BP_BELT), 0)')
|
||
if nbelt != 9:
|
||
print('DEBUG belt row', ev(f'''JSON.stringify((() => {{ const o = []; for (let bx = {bbx} - 6; bx <= {bbx} + 2; bx++) {{ const b = {bby} * BW + bx; o.push([bx, blockP[b], blockInReach(bx, {bby}), (() => {{ const r = []; for (let y = {bby}*4; y < {bby}*4+4; y++) for (let x = bx*4; x < bx*4+4; x++) {{ const m = mat[y*W+x]; if (m) r.push(MD[m].name); }} return [...new Set(r)].join('/'); }})()]); }} return o; }})())'''), ev('[bot.x, bot.y]'))
|
||
check('belt blocks drawn by drag', nbelt == 9, f'{nbelt} {ev("GAME.status")}')
|
||
check('belts dragged to the right run right', ev(f'(() => {{ for (let bx = {bbx} - 6; bx <= {bbx} + 2; bx++) if (blockV[{bby} * BW + bx] !== 1) return false; return true; }})()'))
|
||
# copy: hold C and drag a box over them
|
||
pg.mouse.move(*blk(bbx - 6, bby)); settle(300); pg.keyboard.down('c'); settle(200)
|
||
check('holding C shows copy on the card', pg.is_visible('#selcard') and 'Copy' in ev("$('selcard').textContent") and ev("selModeHeld()") == 'copy', ev("$('selcard').textContent")[:80])
|
||
pg.keyboard.up('c'); settle(100)
|
||
drag(lambda: cell_to_screen((bbx - 6) * 4 + 1, bby * 4 + 1), lambda: cell_to_screen((bbx + 2) * 4 + 2, bby * 4 + 2), 5, hold_key='c')
|
||
check('blueprint copied', ev('!!GAME.clip && GAME.paste && GAME.clip.blocks.length === 9'), ev('GAME.status'))
|
||
pg.mouse.move(*blk(bbx - 2, bby - 9)); settle(150)
|
||
pg.screenshot(path=str(out / '07-paste-preview.png'))
|
||
pg.mouse.down(); pg.mouse.up()
|
||
settle(1500)
|
||
nbelt2 = ev('blockP.reduce((a, v) => a + (v === BP_BELT), 0)')
|
||
check('ghosts get built from the bag', nbelt2 == 18, f'belts={nbelt2} ghosts={ev("ghostBlocks.size")}')
|
||
m_before = ev('money')
|
||
pg.keyboard.press('Control+z'); settle(300)
|
||
check('Ctrl+Z undoes the paste: what was built from it comes back down, refunded', ev('blockP.reduce((a, v) => a + (v === BP_BELT), 0)') == 9 and ev('ghostBlocks.size') == 0 and ev('money') > m_before, ev('[blockP.reduce((a, v) => a + (v === BP_BELT), 0), ghostBlocks.size, GAME.status]'))
|
||
pg.keyboard.press('Escape'); settle(150)
|
||
check('Esc stops pasting (and does not open the menu)', not ev('GAME.paste') and ev("$('m-pause').hidden"))
|
||
# a line dragged right to left runs left
|
||
ev(f"for (let y = {(bby+1)*4}; y < {(bby+2)*4}; y++) for (let x = {(bbx-8)*4}; x < {(bbx+4)*4}; x++) if (!blockP[((y/4)|0)*BW + ((x/4)|0)]) setCell(y*W+x, EMPTY, 20, 0);")
|
||
ok = select('3', "piece().id === 'belt'")
|
||
drag(lambda: blk(bbx - 2, bby + 1), lambda: blk(bbx - 6, bby + 1))
|
||
check('belts dragged to the left run left, and the card follows', ev(f'(() => {{ for (let bx = {bbx} - 6; bx <= {bbx} - 2; bx++) if (blockP[({bby} + 1) * BW + bx] !== BP_BELT || blockV[({bby} + 1) * BW + bx] !== 0) return false; return true; }})()') and ev('GAME.pieceDir.belt') == 0, ev(f'[blockP[({bby} + 1) * BW + {bbx} - 4], blockV[({bby} + 1) * BW + {bbx} - 4], GAME.status]'))
|
||
# cut: hold X and drag; it all comes down into the bag, ready to paste; Ctrl+Z puts it back
|
||
m_before = ev('money')
|
||
drag(lambda: cell_to_screen((bbx - 6) * 4 + 1, (bby + 1) * 4 + 1), lambda: cell_to_screen((bbx - 2) * 4 + 2, (bby + 1) * 4 + 2), 5, hold_key='x')
|
||
check('X-drag cuts the row: taken down, refunded, ready to paste', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == 0 and ev('money') > m_before and ev('GAME.paste && GAME.clip.blocks.length === 5'), ev('[money, GAME.status]'))
|
||
pg.keyboard.press('Escape'); settle(100)
|
||
pg.keyboard.press('Control+z'); settle(300)
|
||
check('Ctrl+Z puts the cut row back', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == ev('BP_BELT') and ev('money') == m_before, ev('[money, GAME.status]'))
|
||
# G with a machine: it replaces the belt it is placed on
|
||
ok = select('3', "piece().id === 'fan'")
|
||
pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(400)
|
||
pg.mouse.down(); pg.mouse.up(); settle(100)
|
||
check('a fan on a belt says to hold G', 'Hold G' in ev('GAME.status'), ev('GAME.status'))
|
||
pg.keyboard.down('g'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200)
|
||
check('holding G shows replace mode', 'Replace · G' in ev("$('selcard').textContent"))
|
||
pg.screenshot(path=str(out / '07g-replace-fan.png'))
|
||
pg.mouse.down(); pg.mouse.up(); settle(100); pg.keyboard.up('g'); settle(100)
|
||
check('G-click puts the fan in place of the belt', ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4 && m.y === ({bby} + 1) * 4)") and ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == 0, ev('GAME.status'))
|
||
x_before, y_before = ev('[bot.x, bot.y]')
|
||
pg.keyboard.down('g'); pg.keyboard.down('d'); settle(300)
|
||
pg.keyboard.up('d'); pg.keyboard.up('g')
|
||
check('flying with WASD works while G (replace) is held', ev('bot.x') > x_before, ev('bot.x') - x_before)
|
||
ev(f'bot.x = {x_before}; bot.y = {y_before}; R.snapNext = true;'); settle(200)
|
||
# pipes are laid by dragging, like belts
|
||
ok = select('3', "piece().id === 'pipe'")
|
||
check('pipes, pumps, outlets and the refiner are in the build menu', ok and all(x in ev("PIECES.filter(p => !p.hidden).map(p => p.id)") for x in ['pipe', 'pump', 'outlet', 'refiner']))
|
||
ev(f"for (let y = {(bby+2)*4}; y < {(bby+3)*4}; y++) for (let x = {(bbx-8)*4}; x < {(bbx+4)*4}; x++) if (!blockP[((y/4)|0)*BW + ((x/4)|0)]) setCell(y*W+x, EMPTY, 20, 0);")
|
||
drag(lambda: blk(bbx - 6, bby + 2), lambda: blk(bbx - 3, bby + 2))
|
||
check('dragging lays a line of pipe that joins into one line', ev(f'(() => {{ let n = 0; for (let bx = {bbx} - 6; bx <= {bbx} - 3; bx++) if (blockP[({bby} + 2) * BW + bx] === BP_PIPE) n++; return n; }})()') == 4 and ev(f'pipeNetOf(({bby} + 2) * BW + {bbx} - 6).blocks.length') == 4, ev('GAME.status'))
|
||
ok = select('3', "piece().id === 'fan'")
|
||
pg.keyboard.press('Control+z'); settle(200)
|
||
pg.keyboard.press('Control+z'); settle(200)
|
||
check('and Ctrl+Z swaps the belt back', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == ev('BP_BELT') and not ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4)"), ev('GAME.status'))
|
||
|
||
# 7b. filter: choose several materials with F, place a line, replace belts with G, edit the whole line
|
||
ev('seen[CUORE] = 1; seen[FEORE] = 1; seen[SAND] = 1; money = 1000; GAME.filterCfg = { m: [], inv: 0 };')
|
||
ok = select('3', "piece().id === 'filter'")
|
||
check('select filter', ok)
|
||
pg.keyboard.press('f'); settle(200)
|
||
check('F opens the filter picker', ev("!$('m-dialog').hidden") and pg.locator('#dlg-body .fp').count() >= 3, pg.locator('#dlg-body .fp').count())
|
||
pg.locator('#dlg-body .fp', has_text='Copper ore').click(); pg.locator('#dlg-body .fp', has_text='Iron ore').click(); pg.locator('#dlg-body .fd', has_text='Right').click(); settle(100)
|
||
pg.screenshot(path=str(out / '07b-filter-picker.png'))
|
||
pg.locator('#dlg-foot button', has_text='Set filter').click(); settle(150)
|
||
check('picking sets several materials and the direction', ev('JSON.stringify(GAME.filterCfg.m) === JSON.stringify([CUORE, FEORE].sort((a, b) => a - b)) && GAME.filterCfg.inv === 0 && GAME.pieceDir.filter === 1') and ev("$('m-dialog').hidden"), ev('JSON.stringify([GAME.filterCfg, GAME.pieceDir.filter])'))
|
||
fy = bby - 5
|
||
ev(f"for (let y = {fy*4 - 12}; y < {fy*4 + 12}; y++) for (let x = {(bbx-8)*4}; x < {(bbx+2)*4}; x++) if (!blockP[((y/4)|0)*BW + ((x/4)|0)]) setCell(y*W+x, EMPTY, 20, 0);")
|
||
drag(lambda: blk(bbx - 6, fy), lambda: blk(bbx - 3, fy))
|
||
nf = ev('blockP.reduce((a, v) => a + (v === BP_FILTER), 0)')
|
||
check('filters placed by drag, dropping both ores and carrying right', nf == 4 and ev(f'blockV[{fy}*BW+{bbx-5}]') == 128 and ev(f'filterInfo({fy}*BW+{bbx-5}).m.length') == 2, f"{nf} {ev(f'JSON.stringify(filterInfo({fy}*BW+{bbx-5}))')}")
|
||
# two belts at the end of the line: plain dragging leaves them, G-dragging swaps them for filters
|
||
ev(f"placeBlock(BP_BELT, 1, {bbx-2}, {fy}, true); placeBlock(BP_BELT, 1, {bbx-1}, {fy}, true);")
|
||
drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy))
|
||
check('dragging over built belts leaves them and says how to replace', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_BELT') and 'Hold G' in ev('GAME.status'), ev('GAME.status'))
|
||
pg.keyboard.down('g'); settle(50)
|
||
pg.mouse.move(*blk(bbx - 2, fy)); settle(300)
|
||
pg.screenshot(path=str(out / '07c-replace-preview.png'))
|
||
drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy))
|
||
pg.keyboard.up('g'); settle(50)
|
||
check('G-dragging replaces them with filters', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_FILTER && blockP[{fy}*BW+{bbx-1}] === BP_FILTER') and ev(f'filterRun({fy}*BW+{bbx-5}).length') == 6, ev('GAME.status'))
|
||
pg.mouse.move(*blk(bbx - 5, fy)); settle(200)
|
||
check('hovering a filter says what it drops and carries', 'copper ore' in ev('JSON.stringify(hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)))').lower())
|
||
ev(f"for (let y = {fy*4 - 10}; y < {fy*4 - 1}; y++) for (let x = {(bbx-6)*4}; x < {(bbx-5)*4 + 2}; x++) setCell(y*W+x, [SAND, CUORE, FEORE][(x+y)%3], 20, 0);")
|
||
settle(500)
|
||
pg.screenshot(path=str(out / '07d-filter-working.png'))
|
||
pg.keyboard.press('1'); settle(60)
|
||
pg.mouse.move(*blk(bbx - 4, fy)); settle(150)
|
||
pg.keyboard.press('f'); settle(200)
|
||
check('F on a placed filter opens the picker for its whole line', ev("!$('m-dialog').hidden") and '6 in this row' in pg.locator('#dlg-title').inner_text() and ev('GAME.editRun && GAME.editRun.length') == 6, pg.locator('#dlg-title').inner_text())
|
||
pg.locator('#dlg-body .fm', has_text='Carry these').click(); pg.locator('#dlg-body .fp', has_text='Copper ore').click(); pg.locator('#dlg-body .fp', has_text='Iron ore').click(); pg.locator('#dlg-body .fp', has_text='Sand').click(); settle(100)
|
||
pg.screenshot(path=str(out / '07e-filter-edit-line.png'))
|
||
pg.locator('#dlg-foot button', has_text='Set filter').click(); settle(150)
|
||
check('and changes the whole line to carry sand and drop the rest', ev(f'filterRun({fy}*BW+{bbx-6}).length') == 6 and ev(f'filterInfo({fy}*BW+{bbx-1}).inv') == 1 and ev(f'JSON.stringify(filterInfo({fy}*BW+{bbx-6}).m) === JSON.stringify([SAND])') and not ev('GAME.editRun'))
|
||
pg.mouse.move(*blk(bbx - 4, fy - 12)); settle(150)
|
||
pg.keyboard.press('i'); settle(300)
|
||
check('I turns on the info overlay', ev('GAME.infoOverlay === true'))
|
||
check('the info overlay is named on screen and framed', 'Info overlay' in ev("$('ovbadges').textContent") and 'info' in ev("$('ovframe').className"), pg.inner_text('#ovbadges'))
|
||
pg.screenshot(path=str(out / '07f-filter-tags.png'))
|
||
pg.keyboard.press('i'); settle(100)
|
||
check('and off again', ev('!GAME.infoOverlay'))
|
||
|
||
# 8. zoom range
|
||
for _ in range(12): pg.mouse.wheel(0, -100); pg.wait_for_timeout(20)
|
||
check('zoom reaches 12', ev('SET.zoom') == 12, ev('SET.zoom'))
|
||
pg.screenshot(path=str(out / '08-zoom12.png'))
|
||
for _ in range(8): pg.mouse.wheel(0, 100); pg.wait_for_timeout(20)
|
||
|
||
# 9. notebook + save + quit + continue
|
||
pg.keyboard.press('n'); settle(200)
|
||
check('notebook opens', ev("!$('m-notebook').hidden"))
|
||
ev("nbTab = 'Pieces'; renderNotebook()"); settle(100)
|
||
pg.screenshot(path=str(out / '09-notebook-pieces.png'))
|
||
ev("nbTab = 'Ledger'; renderNotebook()"); settle(100)
|
||
pg.screenshot(path=str(out / '09b-notebook-ledger.png'))
|
||
ev("nbTab = 'Blueprints'; renderNotebook()"); settle(100)
|
||
pg.screenshot(path=str(out / '09c-notebook-blueprints.png'))
|
||
pg.locator('[data-starter]').first.click(); settle(150)
|
||
check('starter design ready to place', ev("!!GAME.clip && GAME.paste && GAME.clip.molds.length === 1"))
|
||
pg.mouse.move(*blk(bbx + 2, bby - 16)); settle(200)
|
||
pg.screenshot(path=str(out / '09d-starter-ghost.png'))
|
||
pg.keyboard.press('Escape'); settle(100) # stop pasting
|
||
pg.keyboard.press('n'); settle(150)
|
||
pg.keyboard.press('Escape'); settle(100)
|
||
pg.keyboard.press('Escape'); settle(100)
|
||
check('pause opens', ev("!$('m-pause').hidden"))
|
||
pg.click('#pm-save'); settle(800)
|
||
bag_before = ev('bagTotal'); blocks_before = ev('blockP.reduce((a,v)=>a+(v>0),0)')
|
||
pg.click('#pm-title'); settle(1200)
|
||
check('back at title with continue', ev("!$('title').hidden && !$('btn-continue').hidden"))
|
||
pg.click('#btn-continue'); pg.wait_for_function('GAME.running === true', timeout=10000); settle(500)
|
||
check('continue restores bag and blocks', ev('bagTotal') == bag_before and ev('blockP.reduce((a,v)=>a+(v>0),0)') == blocks_before, f"{bag_before}->{ev('bagTotal')}")
|
||
|
||
# 9b. selling: goods sell the moment they drop into the dock; orders pay a lump sum and send a truck
|
||
ev("SET.cardSeconds = 9; orders = []; nextOrderT = 1e12; R.floaters = [];")
|
||
bot_home = ev('[bot.x, bot.y]')
|
||
ev("{ const dk = dock(); bot.x = dk.x + 12; bot.y = dk.y - 26; R.snapNext = true; }"); settle(400)
|
||
m0 = ev('money')
|
||
ev("{ const dk = dock(); for (let y = dk.y + 2; y < dk.y + 8; y++) for (let x = dk.x + 2; x < dk.x + 22; x++) setCell(y*W+x, (x+y)&1 ? CUORE : COAL, AMB[y], 0); }")
|
||
pg.wait_for_function(f'money > {m0} + 50', timeout=10000); settle(150)
|
||
check('goods are paid for as they drop through the grate', ev('money') > m0 + 50, f"{m0} -> {ev('money')}")
|
||
try: pg.wait_for_function('(R.floaters || []).length > 0', timeout=4000)
|
||
except Exception: pass
|
||
check('a "+$" floats up from the dock', ev('(R.floaters || []).length') > 0, ev('JSON.stringify([floatAcc, EV.length, tick, GAME.paused])'))
|
||
pg.screenshot(path=str(out / '09c-dock-sale.png'))
|
||
settle(300)
|
||
check('the wallet shows the new money', pg.locator('#w-money').inner_text().replace(',', '') == '$' + str(round(ev('money'))), pg.locator('#w-money').inner_text())
|
||
ev("seen[BARCU] = 1; orders.push({ id: 77, m: BARCU, qty: 2, got: 0, mult: 2, pay: 160, due: tick + 36000, mins: 10 }); orders.push({ id: 78, m: CUORE, qty: 300, got: 120, mult: 1.8, pay: 540, due: tick + 9000, mins: 5 }); hudDirty = true;")
|
||
settle(200)
|
||
check('the wallet lists open orders', pg.locator('#w-orders .ord').count() == 2, pg.locator('#w-orders').inner_text())
|
||
pg.screenshot(path=str(out / '09d-wallet-orders.png'), clip={'x': 0, 'y': 0, 'width': 1280, 'height': 260})
|
||
m1 = ev('money')
|
||
ev("{ const dk = dock(); spawnBar(BARCU, dk.x + 6, dk.y + 2, 0, 0); spawnBar(BARCU, dk.x + 12, dk.y + 2, 0, 0); }")
|
||
pg.wait_for_function('ordersDone > 0', timeout=10000); settle(200)
|
||
check('filling an order pays its lump sum', abs(ev('money') - m1 - 160) < 1, f"{m1} -> {ev('money')}")
|
||
check('an order-filled card appears, with no truck in it', pg.locator('.card-pop', has_text='Order filled').count() >= 1 and pg.locator('.card-pop', has_text='truck').count() == 0)
|
||
ev(f"bot.x = {bot_home[0]}; bot.y = {bot_home[1]}; R.snapNext = true;"); settle(400)
|
||
pg.click('#wallet'); settle(250)
|
||
check('the wallet opens the Market page', ev("!$('m-notebook').hidden && nbTab === 'Market'"))
|
||
d0 = ev('debt'); pg.locator('[data-pay="all"]').click(); settle(200)
|
||
check('paying toward the claim lowers the debt', ev('debt') < d0 and ev('money') == 0, f"debt {d0} -> {ev('debt')}")
|
||
pg.screenshot(path=str(out / '09f-market.png'))
|
||
pg.keyboard.press('Escape'); settle(150)
|
||
|
||
# 10. mocked gamepad
|
||
ev('''() => {
|
||
const mk = () => ({ connected: true, axes: [0,0,0,0], buttons: Array.from({length:17}, () => ({pressed:false, value:0})) });
|
||
window.__pad = mk();
|
||
navigator.getGamepads = () => [window.__pad];
|
||
}''')
|
||
pg.keyboard.press('1'); settle(100)
|
||
d0 = ev('stats.dug')
|
||
ev("__pad.axes[3] = 0.9; __pad.buttons[7].pressed = true; __pad.buttons[7].value = 1;")
|
||
settle(1500)
|
||
ev("__pad.buttons[7].pressed = false; __pad.buttons[7].value = 0;")
|
||
settle(200)
|
||
check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}")
|
||
ev("__pad.buttons[5].pressed = true;"); settle(100); ev("__pad.buttons[5].pressed = false;"); settle(100)
|
||
check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1)
|
||
ev("__pad.buttons[9].pressed = true;"); settle(100); ev("__pad.buttons[9].pressed = false;"); settle(100)
|
||
check('gamepad Start opens menu', ev("!$('m-pause').hidden"))
|
||
ev("__pad.buttons[13].pressed = true;"); settle(80); ev("__pad.buttons[13].pressed = false;"); settle(80)
|
||
check('gamepad d-pad moves menu focus', ev("document.activeElement && document.activeElement.tagName") == 'BUTTON')
|
||
ev("__pad.buttons[1].pressed = true;"); settle(80); ev("__pad.buttons[1].pressed = false;"); settle(80)
|
||
check('gamepad B closes menu', ev("$('m-pause').hidden"))
|
||
pg.screenshot(path=str(out / '10-after.png'))
|
||
|
||
# 10b. world files: export from the pause menu as an ordinary download, import on the title screen
|
||
pg.keyboard.press('Escape'); settle(150)
|
||
with pg.expect_download() as dl_info:
|
||
pg.click('#pm-export')
|
||
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)
|
||
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.set_input_files('#importfile', dl.path())
|
||
pg3.wait_for_function('GAME.running === true', timeout=60000)
|
||
got = pg3.evaluate('JSON.stringify([W, H, Math.round(money), Math.round(debt), bagTotal])')
|
||
check('importing it on the title screen brings the same world back', got == want, f'{want} -> {got}')
|
||
pg3.close()
|
||
pg.keyboard.press('Escape'); settle(150)
|
||
|
||
# 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)
|
||
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)
|
||
check('large world generates', pg2.evaluate('W === 4096 && H === 1536'), pg2.evaluate('[W, H]'))
|
||
pg2.evaluate("bot.x = 2000; bot.y = magmaY[2000] - 60; snapCamera();"); pg2.wait_for_timeout(800)
|
||
pg2.screenshot(path=str(out / '11-deep-magma.png'))
|
||
pg2.close()
|
||
print('\n'.join(logs) or 'no console errors')
|
||
print('FAILS:', fails)
|
||
b.close()
|
||
sys.exit(1 if fails else 0)
|