Test it without a browser
Headless tests with simulate, the chaos scenarios, and what check and doctor look at.
The engine runs with no DOM and no GPU. @onceworlds/engine/test is how you (and the AI tool building the game) check a game in
seconds.
// test/match.test.js
import { simulate } from '@onceworlds/engine/test';
import { expect, test } from 'vitest';
import { config } from '../game.config.js';
test('a full match ends with everyone ranked, through a reloaded host and a dropped player', async () => {
const run = await simulate(config, { humans: 2, bots: 2, seed: 7, minutes: 3, chaos: ['reload-host', 'drop-player'], latency: [10, 60] });
expect(run.errors).toEqual([]);
expect(run.ranking).toHaveLength(4);
});
onceworlds checkdoes the same for you with a set of chaos runs, plusnpm test. It is the gate before a push.- Chaos scenarios are the bugs real games had:
reload-host,reload-player,drop-player,host-change,pause-during-host-change(the host pauses the match, the role moves, the new host resumes it),late-joiner,garbage-messages(wrong shapes, other players' entities, floods, junk over shared records),forged-state(forgeries of what only the host writes) andhost-leaves. Same seed, same run: a failing seed is a bug report. Add your own:{ name, at, run(env) }. - Test the rules, not the pixels. Step the game (
game.step(n),game.run(seconds)) and assert on components and resources: that a hit takes health, a pickup scores once, a round ends and ranks, a save round-trips, a bot reacts. Drive input withinput.setAxis('move', 1, 0)andinput.tap('jump'); replay a recorded run withinput.play(recording). - Controllers have exact numbers (a jump reaches
jumpHeight), the physics is deterministic, and UI layouts can be checked withui.preview(tree, { width: 390, height: 844, touch: true })(buttons at least 56 px, nothing off screen). - Before you ship: break it with three players (reload the host, close a window, let the match pause), and play it once alone
from the title to the results. The platform guide's checklists ("Before you ship a multiplayer game", "Before you ship any
game") still apply;
checkautomates most of the first.
What onceworlds check, doctor and the platform each look at
check: everythingdoctorfinds, the game's tests (npm test), simulated matches with chaos (needsgame.config.js, or a project'sproject.json, andnpm install), the budgets below, and the end state of a match (game.debug.text(): what is wrong first, then the worlds).doctor: the lints below, a missing viewport tag, text fields under 16 px, a missing icon, thumbnails orcontrols, an engine import with no"engine"inonceworlds.json, and "How to play" out of step with the action map.--fixwritescontrols.--strictfails on likely bugs, for CI.- A deploy checks the file types and limits, the
engineversion (and says which exist), the game page fields, and your repo.
The lints
Each finding names the file and line and says what to write instead. They read the code's text, so they can be wrong: a comment
// onceworlds-ignore <id> on the line (or the line above) silences one.
| Id | What it finds |
|---|---|
sim-random |
Math.random(), Date.now() or performance.now() inside a system or a script's fixed hook |
timers |
setTimeout or setInterval in game code (a Timer, node.timer or time.dt instead) |
dom-in-system |
document, window or localStorage inside a system or a script hook |
backend-import |
an import of three.js or Rapier |
inner-html |
innerHTML, outerHTML, insertAdjacentHTML or document.write |
host-state |
a system without authority: 'host' that writes a component the host owns (net: { owner: 'host' }) |
unbounded-spawn |
a system that spawns every step with no despawn or cap in sight, or a while (true) |
big-file |
a script over 1,500 lines or 300 KB, an image over 2 MB or 4096 px, a sound over 3 MB |
touch |
an action map nothing on a phone can use, or more than four touch buttons; and (a note) a button with no touch or an axis with no stick |
how-to-play |
an engine game with no "controls" in onceworlds.json |
contrast |
text colors too close to the background they are drawn on (under 3:1) |
Budgets
check measures the first player's page during the simulated matches and compares the busiest moment with what each quality tier
of the platform's Graphics setting can draw at 60 frames a second on the devices it is for (low: a phone from a few years ago,
medium: a recent phone or a laptop, high: a desktop).
| Per frame | low | medium | high |
|---|---|---|---|
| 2D draw commands | 1,500 | 3,000 | 5,000 |
| 3D draw calls | 150 | 300 | 600 |
| 3D lights | 6 | 12 | 24 |
| Physics bodies | 150 | 300 | 500 |
| Entities | 4,000 | 8,000 | 15,000 |
Over the low budget is a note (phones will drop frames: batch with instanced meshes, merge static scenery, pool and despawn);
over the high budget is a likely problem. Entities that keep growing to the end of a run are reported too: something is spawned and
never despawned.