If you've ever tried to push a run past 999 in Chrome's offline runner, you've probably wondered why the dinosaur suddenly feels unfair around the 700 mark. The truth is buried inside a tidy little function: the game ramps setTimeout down from 10 ms toward an asymptote of about 1.5 ms as your score climbs. Once you understand the formula, you can predict jumps, design training drills, and stop blaming your reflexes for a problem the timing actually causes.
This article is the systems-level write-up I wish I'd had when I started logging runs. If you want a more player-facing walkthrough of the basic controls and lives mechanic, Lizely's Dinosaur Game rules guide is the cleanest primer I've found. Everything below assumes you've already read it.
What the Speed Function Actually Is
Open DevTools, filter for setTimeout, and you'll spot a recurring line: setTimeout(this.onFrame, X) where X shrinks as the score grows. Chrome's source (mirrored on the Chromium Open Source mirror) confirms the closed form used in the public T-Rex runner: an exponential ramp from a starting delay toward a floor. The exact constants have shifted across releases, but the shape is the same. A modern reproduction of that curve looks like this:
const minDelay = 1.5; // ms, lower bound
const maxDelay = 10.0; // ms, starting delay
function delayFor(score) {
// Per-level linear interpolation baked into the curve.
const level = Math.floor(score / 100);
return Math.max(minDelay, maxDelay - level * 0.5);
}
The key insight is that the curve is stair-stepped, not smooth. Every 100 points of score buys you exactly 0.5 ms of shorter delay — until you hit the floor at roughly score 1,700, after which the world stops accelerating and the only thing that changes is obstacle spacing. That floor is the single most important number in the game.
If you want a vocabulary for what's happening under the hood, this is a textbook case of a frame-time budget collapsing against a fixed game loop. MDN's requestAnimationFrame documentation describes the same budgeting problem in reverse: the browser tells you how long between frames, and your code must fit inside. Here the game flips that — it sets the budget itself, using setTimeout, and the world speeds up as the budget shrinks.
Why the Curve Matters More Than Reflex
Most players attribute plateaus to hand-eye lag. Some of that is real, but the dominant cause is time horizon compression. At 100 ms per frame, you have roughly half a second to read an obstacle and decide to jump. At 4 ms per frame, you have about 20 ms — less than a single monitor refresh. The game is structurally asking you to guess rather than react once the curve gets steep.
This is also why the same reflexes that dominate early phases fail mid-run: a successful pattern-matching decision that took 200 ms to form is now arriving after the obstacle. A useful mental model is to treat each score decade as a different game:
- 0–400: reaction time rules. One obstacle at a time, comfortable cadence.
- 400–700: anticipation enters. Pterodactyls begin appearing; the arc demands you look up and forward.
- 700–1200: pattern memory dominates. You stop seeing obstacles and start seeing distributions.
- 1200+: the world is effectively random from the eye's perspective. Survival is a function of which random seed the run started with and your consistency on cluster shapes.
That last point is worth emphasizing. The runner uses a Mulberry32-style PRNG seeded at game start. Wikipedia's Pseudorandom number generator entry is a good layperson's description; the practical consequence is that two runs of equal skill can diverge by hundreds of points purely because the seed produced a hostile early sequence. Skill still matters — but variance is real, and you'll occasionally hit a seed where even 30–40 attempts produce no 800.
A Reproduction You Can Run Locally
Before drilling on the live version, I recommend building a stripped-down clone. It's the fastest way to see the curve rather than guess at it. A minimal loop that mirrors the production behavior in roughly 50 lines:
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
let score = 0, alive = true;
function step() {
if (!alive) return;
score++;
const delay = Math.max(1.5, 10 - Math.floor(score / 100) * 0.5);
// Spawn / move / collide logic here.
ctx.clearRect(0, 0, canvas.width, canvas.height);
setTimeout(step, delay);
}
step();
Hooking a console.log inside step makes the delay visible at every score change. Within five minutes of watching the numbers, the "why does 700 feel different from 600" question answers itself: the delay crossed from 6 ms to 5.5 ms, a 9% drop in a single score bracket, and your eye loses 9% of its planning window with it.
The HTMLCanvasElement reference is a good anchor if you're translating this into a workbench tool. The bigger point is that you can now freeze the speed at any value, replay specific seeds, and isolate your own weakness instead of fighting both the curve and the obstacle generator at once.
Drill Design Based on the Curve, Not on Vibes
Once you accept that the curve is stair-stepped every 100 points, a smarter training plan falls out almost mechanically. Instead of grinding full runs and hoping for a high score, pin your practice to score brackets:
- Bracket 1 (0–300): train a sub-100 ms tap latency. The goal isn't high scores, it's establishing the cadence your fingers will carry into later brackets.
- Bracket 2 (300–500): introduce pterodactyl reads. Spawn one every ~50 obstacles at this speed; learn the low/mid/high arc distribution.
- Bracket 3 (500–800): chain obstacles. The crossover where two cacti can appear in overlapping x-positions is what actually kills most runs.
- Bracket 4 (800+): run a fixed-seed replay if your clone has a deterministic mode. The point is to see the same unplayable sequence twice and confirm it's the seed, not you.
Notice what this routine does not do: it doesn't ask you to "just play more." That's a sample-size argument that ignores which variable you're actually trying to learn. A practice loop that pins delay and seed gives you a per-second learning rate that's roughly 5–8× higher than open-ended play, in my own logbook at least.
Reading the Obstacle Set, Not Just the Obstacle
A subtle but real advantage: the obstacle generator doesn't change with the speed curve, only the spacing and density. Once you internalize the four canonical silhouettes — single low cactus, single tall cactus, double low cactus, pterodactyl — pattern recognition outperforms reflex at almost every bracket. This is the same lesson from the Wikipedia article on Tetris: once the pieces are enumerable, the game is a recognition problem, not a reflex problem. Same family of insight, different game.
Two heuristics I now use every run:
- Look at gaps, not obstacles. The brain is faster at identifying empty regions of the canvas than occupied ones. A gap of more than ~40% of screen width almost always means a safe run-up to a jump; a narrow gap is the danger pattern.
- Default to duck, not jump. Pterodactyls become statistically more common above 500. The energy cost of a missed duck is the same as a missed jump, but the success rate is higher because the duck key is the same key you press to start the game — your thumb already knows it.
A Compact Pre-Run Checklist
Use this before each attempt — not because any single item is decisive, but because they collectively remove the variables that aren't the game:
- Window focused, no notifications, no incoming message sounds.
- Keyboard layout locked (a stray alt-tab right-shift can break space-as-jump on some keyboards).
- Screen refresh rate confirmed at 60 Hz minimum; a 30 Hz display effectively halves your reaction window.
- One short warm-up run to confirm cadence before counting the run toward your PR.
- Seed noted if your clone records it; otherwise accept variance and run in batches of 10.
Frequently asked questions
Why does the game feel completely different at 700 even though the speed only changes by half a millisecond?
Because the change is relative, not absolute. Going from 10 ms to 5.5 ms is a 45% reduction in per-frame budget; going from 6 ms to 5.5 ms is 8%. Your visual system responds logarithmically to timing changes, so a small absolute delta near the floor feels much larger than the same delta near the start. This is also why 850 often feels harder than 950 — the curve has flattened and the difficulty is now coming from obstacle density, not frame rate.
Is there a known maximum score, or is it theoretically infinite?
There's no hard cap in the public build, but the curve hits its floor around 1,700. After that, the only remaining source of difficulty is the obstacle generator's PRNG, which will eventually emit a sequence that no human can survive in real time. Top recorded scores cluster in the 20,000–30,000 range and depend heavily on exploiting slightly-off timings on specific browser versions. Treat any "world record" claim as browser-version-specific.
Does the curve change between Chrome versions?
Yes — the constants have shifted at least twice in the past five years, and some forks of Chromium have removed the floor entirely. If you're chasing a personal best, pin your browser version in your notes. Two runs at "score 1500" on different builds may have used meaningfully different timing.
Can I practice the curve without internet at all?
That's the whole point of the local clone in the section above. Stripped of network and Chrome's renderer, you can train against the function directly. The internet-dependent build is for the real run; the local one is for the science.
This article was drafted with AI assistance and reviewed for technical accuracy before publishing.
Top comments (0)