root@kaus.com:~/projects$ cat dino-arena.md
Dino Arena
activePersonal
Kbit · page guide
1 / 4
Use the section map on long studies.
Jump between methodology, results, experiments, limitations, and related writing without losing your place.
2 / 4
The narrative carries the claim.
Read each section as problem, method, evidence, and boundary. Negative results remain part of the story.
3 / 4
Inspect figures alongside the narrative.
Read figures in place, scroll wide charts or tables horizontally, and follow any source links for the underlying evidence.
4 / 4
Keep exploring the corpus.
A deterministic runner for comparing hand-written controllers with explicit observation and planning budgets.
Deterministic, seeded two-lane browser runner built for human-vs-agent play and reinforcement-learning evaluation. One headless simulation drives the browser game, a controller roster, and a benchmark harness.
Technologies
- TypeScript
- Canvas
- Headless simulation
Skills
- Simulation design
- RL evaluation
- TypeScript
Links
Related projects
- 12/12 tests pass (~0.8s); npm run lint and npm run build both clean
- Historical benchmark, 9 fixed seeds: analytical solver and 2 rollout controllers clear all 9
- 3 reactive heuristics clear 0% of seeds, 9-29% median course progress
- 1,351 lines of simulation + test logic (core.ts 509, agents.ts 435, renderer.ts 240, tests 167)
Overview
Dino Arena is a dinosaur-runner browser game rebuilt from the ground up so its simulation is completely deterministic: same seed, same actions in, same outcome out, every time, independent of frame rate, wall-clock time, or Math.random. That property is what turns a toy game into a real evaluation harness. The repo ships eight opponent controllers, from a keyboard-passthrough practice mode up through an open-loop timer, three reactive heuristics, a closed-form analytical solver, and two model-predictive planners, plus a benchmark script that runs the exact same headless simulation the browser uses across nine fixed seeds.
In the July audit at c722e52, the analytical controller Arc Analyst and the two rollout planners, Horizon Scout and Course Cartographer, clear the course on 100% of seeds, while the three reactive heuristics land between 9% and 29% median course progress and never clear a single seed. Both the algorithms and their observation scopes differ, so this benchmark describes the complete controllers rather than isolating the benefit of planning. The real engineering decision is not the dinosaur game itself, it is that the same 509-line simulation module drives a requestAnimationFrame canvas loop in the browser and a plain Node for loop in tests and the benchmark script, through a shared state-transition function
The problem
Browser runner games built for humans, the Chrome offline dinosaur game and its many clones, couple their physics to render timing and generate obstacles with unseeded randomness. That is fine for a person playing casually, but it makes the game useless as a benchmark: run the same controller against it twice and it faces two different obstacle courses, so there is no fair way to say one controller is actually better than another, only that it got luckier.
Dino Arena closes that gap: a small, self-contained runner where the simulation truth, physics, collisions, obstacle placement, and scoring, is fully separated from anything that varies run to run. It works as a game to play by hand, and without any changes, as a controlled environment for comparing hand-written agent policies head to head.
Approach and architecture
The design starts from one constraint and builds outward from it: a single pure function, stepMatch, advances the entire match state by exactly one 1/60-second tick and touches nothing outside its arguments, no DOM, no Canvas, no timers, no random number generator. A course is generated once from an integer seed via a small seeded PRNG and stored as an immutable array shared by both lanes, so a human runner dying never changes what obstacles the AI runner sees next. On top of that core sits a narrow AgentActionSource interface (reset(seed), nextAction(observation)), the entire contract a controller has to satisfy. The autonomous entries implement seven strategies: an open-loop cadence timer that does not inspect obstacles, a fixed-distance reflex threshold, a periodically sampled time-to-collision heuristic, a small approach/duck/recover state machine, a closed-form ballistic jump-window solver, and two model-predictive controllers that roll the ballistic solver forward over a bounded or full-course observation window.
At the historical c722e52 checkpoint, four TypeScript modules under app/game/ did the main work: core.ts (509 lines, the deterministic simulation, world constants, the seeded PRNG, course and match creation, collision, and the 23-value observation vector), agents.ts (435 lines, the eight controllers plus the registry that drives both the UI picker and the benchmark), renderer.ts (240 lines, one DPR-aware Canvas 2D draw function that only reads the match snapshot it is handed), and DinoArena.tsx (332 lines, the only file that touches requestAnimationFrame, keyboard events, and React state, running a fixed-timestep accumulator so the simulation always advances in true 60Hz increments regardless of the browser's actual frame rate). scripts/benchmark-agents.ts and the test files import core.ts and agents.ts directly and drive them in a plain loop with no browser APIs at all, which is the actual proof that the simulation is headless.
architecture · dino-arena
What each controller knows
Replaying the same course controls the environment, but the policies still make different decisions with different information. Clockwork Hopper reads the tick and whether it is grounded without consulting obstacles. The reactive policies inspect the next hazard, with different sampling cadences and thresholds. Arc Analyst calculates a jump window from known physics. The two rollout planners compare three immediate actions, then use the same ballistic policy for the remaining simulated steps.
The table records a September 2026 source inspection, separate from the July benchmark below. Horizon Scout uses the three upcoming obstacles encoded in the ordinary observation. Course Cartographer reconstructs the course from the seed and uses the next six hazards. It also has a longer planning horizon. Those differences make this a comparison of complete controllers, not an experiment that isolates the benefit of planning or extra information.
| Controller | Information used | Decision rule | Cadence / planning budget |
|---|---|---|---|
| Manual Sparring | Human keyboard input | Manual practice | Excluded from autonomous benchmark |
| Clockwork Hopper | Tick and grounded state; seed sets phase; no obstacle data | Open-loop cadence | Jump attempt every 61 ticks |
| Late Reflex | Next obstacle and grounded state | Fixed-distance thresholds | Checks each tick; no rollout |
| TTC Pilot | Next obstacle, runner state and speed | Time-to-collision heuristic | Samples every 8 ticks; no rollout |
| FSM Sentinel | Next obstacle, runner state and speed | Approach / duck / recover state machine | Normally 10-tick sampling, except while ducking; 12-tick jump recovery |
| Arc Analyst | Next obstacle, runner state, speed and known physics | Jump-window solver plus hand-set thresholds | Checks each tick; no rollout |
| Horizon Scout | Up to three encoded upcoming obstacles and runner state | Rollout with shared ballistic continuation policy | 3 initial actions, up to 78 ticks each; replans each tick |
| Course Cartographer | Course reconstructed from seed; next six hazards used | Same rollout and ballistic continuation policy | 3 initial actions, up to 120 ticks each; replans each tick |
Information used describes what each policy reads, not enforced access permissions. The ordinary observation encodes three upcoming hazards, which may extend beyond the screen. The local source contains uncommitted controller and collision changes, so July's benchmark scores remain a separate historical result.
Download controller design, source hashes and line references (JSON)Implementation notes
Game speed depends only on tick count, not on agent behavior: speedAtTick(tick) ramps from 278px/s to a 472px/s cap purely as a function of elapsed ticks, so no controller can finish a shorter run by playing recklessly or a longer one by playing cautiously. The benchmark confirms this directly: Arc Analyst, Horizon Scout, and Course Cartographer all report a median of exactly 6,233 ticks, because they all reach the same finish line at the same simulated moment.
One ballistic solver is reused three times rather than written three times: flightAction(), the closed-form jump-window math, is used directly by Arc Analyst and as the leaf policy scored by a short forward rollout inside Horizon Scout (78-tick lookahead, 3 encoded upcoming obstacles) and Course Cartographer (120-tick lookahead, next six hazards from the reconstructed course). Course Cartographer's advantage is stated honestly as information, not intelligence: the README calls its "Expert" tier "intentionally transparent," since it rebuilds the known deterministic course from the match seed and plans over six hazards instead of three, rather than running a smarter algorithm.
No trained model exists anywhere in this repo, and the README says so directly. Seven autonomous controllers use hand-written deterministic policies, and the eighth entry passes through keyboard input. The DinoEnvironment class exposes a reset/step/serialize adapter with an RL-shaped reward signal, which reads as training-ready scaffolding, but nothing in the repo currently trains against it: no trainer, no replay buffer, no learned weights.
Game speed vs. tick: speedAtTick(tick)
speedAtTick(tick) = min(278 + tick × 0.095, 472)px/s, read directly from app/game/core.ts. Speed is a pure function of elapsed ticks, never of agent behavior, which is why every controller that clears the course finishes at the identical tick. Zero-based y-axis so the ramp isn't visually exaggerated.
figures · dino-arena
Results
Verified against a local checkout at commit c722e52 ("Add deterministic opponent roster"). npm test (12 tests across core and agent behavior) passes 12/12 in about 0.8 seconds. npm run lint is clean. npm run build (vinext, Vite 8.0.13) succeeds across all five build stages.
npm run bench:agents, median across 9 fixed qualification seeds: Clockwork Hopper (open-loop cadence) reaches 1% progress with a 0% clear rate, TTC Pilot (time-to-collision heuristic) 9% progress and 0% clear, Late Reflex (fixed-distance threshold) 13% progress and 0% clear, FSM Sentinel (approach/commit/recover state machine) 29% progress and 0% clear, and Arc Analyst, Horizon Scout, and Course Cartographer all reach 100% progress, 100% clear, at a median of 6,233 ticks.
Line counts verified with wc -l: core.ts 509, agents.ts 435, renderer.ts 240 (1,184 combined), plus 167 lines of tests, for 1,351 lines of simulation and test logic total. DinoArena.tsx (the React UI shell) is a further 332 lines, and the benchmark script is 61 lines, both separate from that figure.
Skill-ladder benchmark: median course progress by agent
Median across 9 fixed seeds (101, 307, 911, 1,337, 2,021, 4,067, 7,777, 12,345, 73,194), TICK_CAP 12,000, from npm run bench:agents. Zero-based bars, one chart, no smoothing. Clear rate is binary here (every agent is either 0% or 100%), so it's annotated per bar rather than plotted as its own axis. Colors identify difficulty tiers. Each row names its controller's algorithm.
figures · dino-arena
Tradeoffs and lessons
Courses are finite, 96 obstacles, roughly 6,200 ticks to clear at the observed pace, not endless. That is the right choice for a benchmark tool, but it does mean Dino Arena is not a "play until you die" clone in the traditional sense. The benchmark's headline number, a 100% clear rate for the two model-predictive controllers against 9% to 29% median progress for the three reactive heuristics, describes these implementations on nine fixed seeds. Information access differs too, so the comparison does not isolate the effect of planning. It does not generalize beyond that: none of the eight controllers are learned policies, so the result is a statement about hand-written control strategies, not a claim about reinforcement learning or any trained agent. The Next.js and Cloudflare Worker hosting shell is scaffolding from the vinext-starter template, not custom engineering. It exists to make the game playable at a URL.
The transferable idea is about evaluation design: comparing two policies fairly requires an environment that is provably identical between runs, and "provably" means removing every non-deterministic input (wall clock, frame rate, Math.random) from the state-transition function itself, rather than trying to control for it statistically after the fact. Building stepMatch as a pure, dependency-free function first, and only afterward wrapping it in a requestAnimationFrame loop for the browser, is what made the headless benchmark and the 12-test suite possible without a second implementation to keep in sync. The difficulty ladder combines observation scope, reaction cadence, hand-set thresholds and planning depth, and the repo names its own scope honestly: no hosted model, no training loop, and no hidden randomness in the seven autonomous opponents, even though the RL-shaped API looks exactly like the seam a real pipeline would plug into.
Where it is heading
The tracked next step is to train a reinforcement-learning agent through the existing environment adapter and publish its learning curves next to the scripted roster's benchmark table. Shared seeded courses make that comparison repeatable. A fair algorithm comparison also requires matched observation access, action timing, and evaluation budgets. Until that experiment exists, the site reports the scripted roster's measured results.
notes
- No trained model exists in this repo. The roster contains seven hand-written autonomous policies and one manual keyboard mode. The RL-shaped DinoEnvironment adapter (reset/step/serialize with a reward signal) is scaffolding, not a finished training pipeline.