// ============ framecheck.mjs โ€” run the REAL game loop for thousands of frames ============ let fails = 0; const ok = (cond, msg) => { console.log((cond ? 'โœ” ' : 'โœ˜ ') + msg); if (!cond) fails++; }; const makeCtx = () => new Proxy(function () {}, { get(t, p) { if (!(p in t)) t[p] = (...a) => makeCtx(); const v = t[p]; return typeof v === 'function' ? v : v; }, set() { return true; }, apply() { return makeCtx(); }, }); const mkEl = (id = '') => ({ id, style: {}, dataset: {}, children: [], _listeners: {}, addEventListener(ev, fn) { (this._listeners[ev] ??= []).push(fn); }, classList: { _s: new Set(['hidden']), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, toggle() {}, contains(c) { return this._s.has(c); } }, appendChild() {}, remove() {}, setAttribute() {}, querySelectorAll: () => [], getBoundingClientRect: () => ({ left: 0, top: 0, width: 180, height: 180 }), getContext: () => makeCtx(), width: 300, height: 150, textContent: '', }); Object.defineProperty(mkEl.prototype ?? {}, 'x', { value: 0 }); globalThis.window = globalThis; globalThis.innerWidth = 1280; globalThis.innerHeight = 800; globalThis.__rafQ = []; globalThis.requestAnimationFrame = fn => { globalThis.__rafQ.push(fn); return 1; }; globalThis.cancelAnimationFrame = () => {}; globalThis.addEventListener = () => {}; globalThis.removeEventListener = () => {}; globalThis.localStorage = { _m: {}, getItem(k) { return this._m[k] ?? null; }, setItem(k, v) { this._m[k] = String(v); }, removeItem(k) { delete this._m[k]; } }; const els = {}; const REAL_IDS = new Set(['game','minimap','topbar','toolbar','palette','pal-body','pal-title','pal-close','context-panel','tool-hint','toasts','modal-root','main-menu','mm-new','mm-how','mm-continue','stat-cash','stat-guests','stat-rating','mana-fill','mana-num','stat-weather','stat-date','btn-pause','btn-research','btn-finance','btn-heroes','btn-objectives','btn-park','btn-save','btn-help','minimap-wrap']); globalThis.document = { getElementById(id) { if (!REAL_IDS.has(id)) return null; return els[id] ?? (els[id] = mkEl(id)); }, createElement: () => mkEl(), createTextNode: t => ({ textContent: String(t) }), querySelectorAll: () => [], querySelector: () => null, addEventListener() {}, body: { appendChild() {} }, }; await import('../js/main.js'); const stateM = await import('../js/game/state.js'); const heroesM = await import('../js/game/heroes.js'); const cfg = await import('../js/core/config.js'); for (const scenId of ['meadows', 'sandbox']) { // fresh state through the REAL entry path document.getElementById('main-menu'); // ensure element exists const st = stateM.newGame(scenId); heroesM.cacheScenario(st, cfg.SCENARIOS.find(s => s.id === scenId)); st._speed = 3; // fast-forward st._paused = false; // give the park some content to exercise more code paths const m = st.map; const ex = m.entranceX, ey = m.entranceY; for (let y = ey - 12; y < ey; y++) for (let x = ex - 8; x <= ex + 8; x++) if (m.isBuildable(x, y) && !m.objects[m.idx(x, y)]) m.pathType[m.idx(x, y)] = 1; stateM.addShopObj(st, 'food', ex - 4, ey - 8); stateM.addShopObj(st, 'drinks', ex - 2, ey - 8); stateM.addShopObj(st, 'toilet', ex + 4, ey - 8); const ride = stateM.addRideObj(st, 'carousel', ex - 7, ey - 11, {}); ride.status = 'open'; ride.price = 2; let frames = 0, crashed = null, lastErrFrame = -1; let tNow = performance.now(); for (; frames < 4000; frames++) { const q = [...globalThis.__rafQ]; globalThis.__rafQ.length = 0; tNow += 33; // ~30fps try { for (const f of q) f(tNow); } catch (e) { crashed = e; lastErrFrame = frames; break; } } if (crashed) { ok(false, `[${scenId}] crashed at frame ${lastErrFrame}: ${crashed.message}`); console.error(crashed.stack?.split('\n').slice(0, 6).join('\n')); } else { ok(true, `[${scenId}] ${frames} frames clean ยท guests=${st.guests.length} cash=${Math.round(st.cash)} hour=${st.time.hour.toFixed(1)}`); } } console.log(fails ? `\n${fails} FRAME FAILURES` : '\nREAL LOOP STABLE'); process.exit(fails ? 1 : 0);