A deterministic browser auto-runner with cube, ship and ball modes, a level editor, an endless mode and in-browser reinforcement learning (tabular Q-learning and a DQN). No backend, no physics engine, no game framework โ the simulation is hand-rolled and runs entirely client-side.
Play it at q-runner.vercel.app.
Built with Next.js (App Router) + TypeScript, Zod for the level schema, Zustand for editor state, localForage for persistence, and a plain 2D canvas for rendering.
A fixed 120Hz simulation lives in one pure step function. The game loop, the training environment and a reachability solver all call that same function, which is what makes the central claim hold: a level cannot ship unless the solver has proved it clearable.
The solver is allowed to change its input only once every six ticks โ a 50ms grid. So "solvable"
means solvable with roughly human timing slack, not frame-perfectly. pnpm build runs it over
all ten built-in levels and fails the build on any rejection.
/levelsโ the ten built-in levels plus anything you have built, with best progress and clear state per level./playโ the game. Hold to jump, fly or flip depending on the mode. You can also watch a scripted baseline bot, or watch the solver's own winning route replayed./editorโ pan/zoom canvas, place, erase, marquee multi-select, drag, snapping, undo/redo, an inspector for object and level settings, JSON import/export, and a Check button that runs the level through the same solver the built-in levels pass./endlessโ stages generated on the fly and rejected unless the solver can still cross them. Difficulty and speed climb per stage. The next stage is built while you play the current one, and a run is reproducible from its seed (?seed=โฆ)./trainingโ an agent training against the same engine, with a learning curve, throughput and state-coverage metrics, and a viewer that runs the greedy policy. Pick tabular Q-learning over a discretised state, or a DQN (a small hand-written MLP with experience replay and a target network, no ML library). Agents are saved per level.
Platforms (tile, span, block, floating, ceiling), spikes (single, double, triple, tall, mini, inverted), yellow and pink pads and orbs, cube/ship/ball portals, gravity portals, and a finish gate. Hazards use a hitbox smaller than the drawn spike so clipping a corner is survivable.
pnpm install
pnpm devThe live site is a Vercel project connected to this repo, so pushes to master deploy. Set
NEXT_PUBLIC_SITE_URL to the deployment's origin (see .env.example): it is read at build time
for canonical URLs, the sitemap, robots.txt and the social card, and falls back to
http://localhost:3000 when unset.
| Command | What it does |
|---|---|
pnpm dev |
Dev server |
pnpm build |
Validates every built-in level, then builds |
pnpm check |
Typecheck, lint, tests, level validation |
pnpm test |
95 tests via node:test |
pnpm validate:levels |
Solve every built-in level end to end |
pnpm generate:levels |
Regenerate the ten built-in levels (deterministic per seed) |
pnpm generate:icons |
Redraw favicons and the social card |
pnpm preview [level] [ticksโฆ] |
Render frames of a level to PNG via a headless canvas |
pnpm benchmark:agents |
Train both agents on every built-in level and report how far each gets |
pnpm preview replays a solved route and writes frames to previews/. It is a visual smoke test
for the renderer that does not need a browser.
Levels are version-1 JSON validated by a Zod schema (src/game/level/levelSchema.ts), in world
units rather than pixels โ the same unit the editor grid, the renderer and the solver all use.
The ten built-in levels live in src/data/builtinLevels/ and are generated, not hand-placed:
chunks are appended only if a beam of surviving states can still cross them.
Regenerating is deterministic. Object ids are positional and the authoring timestamp is pinned,
so pnpm generate:levels twice produces byte-identical files.
pnpm test covers the simulation, the solver, generation, the editor store and the renderer.
The one that matters most replays the solver's own winning route through the real GameEngine
for each shipped level and asserts it finishes โ if the engine and the solver ever drift apart,
"validated" quietly stops meaning anything.
pnpm benchmark:agents trains each agent from scratch on every built-in level, then runs its
greedy policy (no exploration) once. Each agent gets the budget its epsilon schedule is tuned for:
1M steps for the Q-table, 200k for the DQN, whose exploration bottoms out near 100k. Exploration
is random, so each pair ran three times; progress columns are medians.
| Level | Q-learning: best in training | Q-learning: greedy | Q-learning clears | DQN: best in training | DQN: greedy | DQN clears |
|---|---|---|---|---|---|---|
| 01 First Light | 100% | 100% | 3/3 | 100% | 18% | 0/3 |
| 02 Static Drift | 100% | 45% | 0/3 | 85% | 33% | 0/3 |
| 03 Overpass | 100% | 72% | 1/3 | 10% | 10% | 0/3 |
| 04 Ion Wake | 44% | 18% | 0/3 | 13% | 9% | 0/3 |
| 05 Crosswire | 100% | 77% | 1/3 | 20% | 19% | 0/3 |
| 06 Nightfall | 64% | 16% | 0/3 | 64% | 17% | 0/3 |
| 07 Rollcage | 22% | 20% | 0/3 | 21% | 19% | 0/3 |
| 08 Inversion | 87% | 11% | 0/3 | 47% | 11% | 0/3 |
| 09 Redline | 99% | 10% | 0/3 | 33% | 7% | 0/3 |
| 10 Terminal Velocity | 100% | 100% | 2/3 | 34% | 8% | 0/3 |
Run on 2026-10-23, about 30โ50s per training run on one CPU core.
- The Q-table is the stronger agent here. Its greedy policy clears four of the ten levels at least once, including the last one. "Best in training" is often far above "greedy" because an exploring run can stumble through a section that the learned policy then fails.
- The DQN never clears a level at these settings. Its network is tiny (16 hidden units), trains on a fifth of the steps, and was not tuned beyond keeping it stable. Read this as "the defaults are not enough", not as a ceiling for the method.
- Clearing is not repeatable. The same agent and budget clear level 03 in one run out of three.
- Progress and levels are per-browser. Everything is in IndexedDB via localForage. There is no account, no sync, and clearing site data loses saved levels.
- Landscape only on mobile. The game is a 16:9 canvas and reads badly in portrait.
- The trained agents are a demo, not a solver. Neither clears every level reliably (see How well the agents learn). The reachability solver is the thing that actually proves a level clearable.
- Rotation is presentation-only. Objects can be rotated for looks; collision stays axis-aligned, which is what the solver assumes.
- No music. There are sound effects for jump, death, orbs and portals;
musicBpmis carried in the level format but nothing reads it yet.
