import re, sys import py_mini_racer as pyr html = open('/workspace/mesh-runner/index.html').read() m = re.search(r'', html, re.S) assert m, 'no inline script found' game_js = m.group(1) HARNESS = r""" var ctxCalls = {}; function makeCtx(){ return new Proxy({}, { get(t, k){ if(k === 'canvas') return {width:960, height:640}; if(typeof k === 'symbol') return undefined; if(!(k in t)){ if(k === 'fillStyle' || k === 'strokeStyle' || k === 'lineWidth' || k === 'font' || k === 'textAlign' || k === 'shadowColor' || k === 'shadowBlur' || k === 'globalAlpha'){ t[k] = ''; return t[k]; } t[k] = function(){ ctxCalls[k] = (ctxCalls[k]||0)+1; return undefined; }; } return t[k]; }, set(t, k, v){ t[k] = v; return true; } }); } var _els = { cv: { id:'cv', width:960, height:640, getContext: function(){ return makeCtx(); } } }; var listeners = {}; var document = { getElementById: function(id){ return _els[id] || null; }, addEventListener: function(t,f){ (listeners[t]=listeners[t]||[]).push(f); } }; var _vt = 0; // virtual clock: 16.67ms per frame, deterministic var performance = { now: function(){ return _vt; } }; var _rafQ = []; function requestAnimationFrame(cb){ _rafQ.push(cb); return _rafQ.length; } function frames(n){ for(var i=0;i= 5, ev('JSON.stringify(Object.keys(ctxCalls))')) # start with Enter key('keydown', 'Enter') frames(5) check('state -> play after Enter', ev('window.__mr.state') == 'play') check('battery full-ish', ev('window.__mr.battery') > 90, ev('window.__mr.battery')) # autopilot soak: autoStep every ~12 frames, top up battery to test scoring not death for i in range(600): if i % 12 == 0: ctx.eval('window.__autoStep();') if ev('window.__mr.battery') < 40: ctx.eval('window.window.__mr.battery = 100;') frames(1) score1 = ev('window.__mr.score'); wave1 = ev('window.__mr.wave'); cb1 = ev('window.__mr.bestCombo') check('autopilot scored points', score1 > 0, f'score={score1} wave={wave1} bestCombo={cb1}') check('autopilot reached wave >= 2', wave1 >= 2, f'wave={wave1}') # jam mechanic: park a jammer on an edge touching a node, verify jam rises ctx.eval(""" (function(){ var j = window.__mr.jammers[0]; j.e = window.__mr.edges[0]; j.t = 0.5; })(); """) frames(30) maxjam = ev('Math.max.apply(null,window.__mr.nodes.map(function(n){return n.jam;}))') check('jammer jams nodes', maxjam > 0.05, f'maxjam={maxjam}') # battery death path ctx.eval('window.__mr.battery = 0.5;') frames(10) check('battery death -> over', ev('window.__mr.state') == 'over') check('best saved to localStorage', ev('window.localStorage.getItem("meshrunner_best")') is not None) # retry key('keydown', 'r') frames(5) check('retry -> play', ev('window.__mr.state') == 'play') check('score reset', ev('window.__mr.score') == 0) # hub bank: combo>=3 landing on hub awards bonus ctx.eval('window.window.__mr.combo = 3; window.__mr.player.node = window.__mr.hub; window.__mr.player.t = 1;') score_before = ev('window.__mr.score') frames(3) score_after = ev('window.__mr.score') check('hub bank awards combo bonus', score_after > score_before, f'{score_before} -> {score_after}') check('combo cleared after bank', ev('window.__mr.combo') == 0) # no JS errors errs = ev('JSON.stringify(window.__errs)') check('no JS runtime errors', errs == '[]', errs) # audio safety: no AudioContext in harness -> sfx must no-op without throwing check('audio-safe without AudioContext', ev('typeof AudioContext') == 'undefined') # mute toggle via M key('keydown', 'm') check('M mutes', ev('window.__mr.muted') == True) key('keydown', 'm') check('M unmutes', ev('window.__mr.muted') == False) # hunter mechanic: force wave 3 rebuild, expect at least one hunter jammer ctx.eval('window.__mr.setWave(3,16);') n_hunters = ev('window.__mr.jammers.filter(function(j){return j.hunter;}).length') check('hunters spawn from wave 3', n_hunters >= 1, f'hunters={n_hunters}') # hunter re-targets toward player without crashing ctx.eval(""" (function(){ var h = window.__mr.jammers.filter(function(j){return j.hunter;})[0]; h.x = 100; h.y = 100; })(); """) frames(120) check('hunter chase runs clean', ev('window.__mr.state') in ('play', 'over')) passed = sum(1 for _, ok in results if ok) print(f'\n{passed}/{len(results)} checks passed') sys.exit(0 if passed == len(results) else 1)