Files
arcane-tycoon/tests/framecheck.mjs
T
deepseek ac00687480 Arcane Tycoon — Heroes & Magic theme park tycoon game
Complete browser game inspired by OpenRCT2 with fantasy twist:
- Custom roller coaster designer with physics-based ratings + on-ride POV
- 10 animated rides, 7 shops, 16 scenery items, path network & guest AI
- Heroes guild vs monster invasions (5 classes, XP/gear/bosses)
- Magic spell system (8 spells), research tree, economy/marketing/loans
- Day-night cycle, weather, park rating, awards, 4 scenarios
- Save/load slots + autosave, procedural WebAudio SFX/music
- Isometric canvas renderer, minimap, diagnostics overlay
- Test suites: smoke(13), linkcheck, inputcheck, framecheck, rendercheck, flow
2026-08-23 06:59:21 +00:00

88 lines
4.1 KiB
JavaScript

// ============ 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);