Tap, click or hit space. The runner never changes speed and there is no left or right, so the only thing you decide all run is when.
Then hit replay without, and watch the same presses you just made go through a controller with the two timers set to zero.
The whole mechanic is two comparison operators
A jump controller normally holds two booleans. grounded, set by the collision
pass. jumpPressed, set by the input pass. Jump when both are true. That
version is correct, and nobody can play it, because both booleans are
instantaneous and the player is not.
Replace each one with the time since it was last true:
// the two booleans, replaced by the time since each was last true
run.sinceGround = run.grounded ? 0 : run.sinceGround + dt;
run.sincePress = pressed ? 0 : run.sincePress + dt;
if (run.sinceGround <= coyote && run.sincePress <= buffer) jump();
// ^^ ^^
// forgives a press that came late ... and one that came early
coyote is coyote time — you may still jump for a few frames after the
floor has run out. buffer is input buffering — a press made in the air is
held, and spends itself the moment you land. They are the same subtraction with
the sign of the error flipped, and both of them cost one <=.
The claim worth checking is that this is not two tricks for one problem. They cover opposite halves of one error distribution, and on any given jump one of them is doing all the work and the other is worth literally nothing.
The course
Three jumps, picked so each one isolates a side.
| jump | gap | slack (reach − gap) | the cue |
|---|---|---|---|
| A | 148 px | 16.2 px = 65 ms | the first ledge |
| B | 110 px | 54.2 px = 217 ms | touchdown on the stub |
| C | 150 px | 14.2 px = 57 ms | the last ledge |
A and C are gaps off a ledge. The jump reaches 164.2 px, so takeoff may be a little early and not one pixel late — the late end of the window is a hard wall, because the floor stops existing.
B is a 50 px stub, 140 px below the ledge you walk off. No jump gets you there: you fall onto it, which lands you on the same pixel every single run and needs no timing at all. Leaving it does. The window opens at touchdown, which is a hard wall on the other side.
That split is the whole design. Everything below falls out of it.
The windows, measured by scanning
Not derived — every jump is swept one frame at a time with the others held at their best offset, and the surviving band reported. Milliseconds relative to that jump’s cue.
| coyote / buffer | A (ledge) | B (stub) | C (ledge) |
|---|---|---|---|
| 0 fr / 0 fr | −33…0 — 50 ms | +17…+100 — 100 ms | −33…0 — 50 ms |
| 5 fr / 0 fr | −33…+83 — 133 ms | +17…+183 — 183 ms | −33…+83 — 133 ms |
| 0 fr / 6 fr | −33…0 — 50 ms | −83…+100 — 200 ms | −33…0 — 50 ms |
| 5 fr / 6 fr | −33…+83 — 133 ms | −83…+183 — 283 ms | −33…+83 — 133 ms |
Read the second column against the first. Six frames of input buffering move a ledge jump’s window by 0 ms. Not nearly nothing — nothing. The early end of a ledge jump is already soft: press early and you jump early, and 16 px of reach slack catches you. There is no early press to save, so there is nothing for a buffer to do.
And look at where the cue sits. For A and C the bare window ends at 0 — the ledge, the only thing on screen that says jump, is the last legal frame.
One frame of B’s +17 is bookkeeping rather than physics: this controller
checks the jump condition before it integrates and resolves collisions, so a
landing on step k is only jumpable on k+1. Swap those two passes and B’s
window starts at 0. That is worth saying out loud, because it is exactly the
class of mistake a buffer hides — at six frames the ordering stops being
observable at all. Buffering is not only a kindness to players; it is also what
stops one frame of engine plumbing from being a gameplay fact.
So the course is not hard. It is mis-cued.
10,000 attempts by a robot that presses once per jump, aimed dead at the cue, with a normally distributed timing error.
| press error σ | 0 / 0 | 5 / 6 |
|---|---|---|
| 0 ms | 0.0% | 100.0% |
| 5 ms | 4.7% | 100.0% |
| 15 ms | 14.5% | 99.3% |
| 30 ms | 11.0% | 83.8% |
| 45 ms | 6.9% | 63.0% |
| 60 ms | 4.3% | 45.4% |
| 80 ms | 2.2% | 28.5% |
The top row is the one to sit with. A player with no timing error whatsoever, pressing exactly on every cue, clears this course zero percent of the time — because jump B’s window opens one frame after touchdown, and a player who is never wrong is never one frame late either. And the column is not monotone: the rate peaks around σ = 15 ms and falls off on both sides. Below that, precision just aims you precisely at a boundary.
That is what coyote time is actually for. It is not forgiveness for bad timing. It is a correction for a cue that is in the wrong place — and the cue is in the wrong place because the only event a platformer can draw is the floor ending, which is by construction the last frame of the window and not the middle of it.
Each timer owns one jump and is worth ~nothing on the other
Same 20,000 attempts, σ = 45 ms, which is about where a human’s press lands.
| coyote / buffer | A | B | C | course |
|---|---|---|---|---|
| 0 fr / 0 fr | 39.5% | 41.8% | 37.9% | 6.6% |
| 5 fr / 0 fr | 79.9% | 42.5% | 80.4% | 27.5% |
| 0 fr / 6 fr | 39.5% | 97.1% | 38.3% | 15.3% |
| 5 fr / 6 fr | 79.9% | 97.9% | 80.0% | 62.9% |
Column A: 39.5 → 79.9 → 39.5 → 79.9. The buffer rows are not approximately
equal to the no-buffer rows, they are the same number, because the buffer
changes nothing about that jump that could change an outcome. Column B:
coyote time moves it 0.7 points and the buffer moves it 55.3.
Together, 6.6% → 62.9%. A 9.5× change in whether a game is playable, out of two
subtractions and two <=.
What a longer window costs
The obvious next question is why anybody stops at five frames. The answer is that the two sides of the ledger grow at different rates.
A coyote jump starts in the air, so it starts in a hole. The hole is
½·g_down·τ² deep and the extra ground it buys is v_run·τ wide — quadratic
against linear — and the jump has to climb back out of the hole before it can
reach the floor it is aiming at.
| coyote | window A | clear | deepest takeoff seen | reach bought | fallen |
|---|---|---|---|---|---|
| 0 fr | 50 ms | 15.4% | 0.0 px | 0.0 px | 0.0 px |
| 3 fr | 100 ms | 51.8% | 2.2 px | 11.5 px | 3.3 px |
| 4 fr | 117 ms | 59.0% | 4.3 px | 14.8 px | 5.8 px |
| 5 fr | 133 ms | 63.1% | 7.2 px | 18.0 px | 9.0 px |
| 6 fr | 150 ms | 65.3% | 10.8 px | 20.8 px | 13.0 px |
| 8 fr | 183 ms | 66.3% | 20.2 px | 25.7 px | 23.1 px |
| 10 fr | 217 ms | 66.5% | 32.5 px | 29.4 px | 36.1 px |
| 12 fr | 250 ms | 66.6% | 47.7 px | 31.5 px | 52.0 px |
| 16 fr | 317 ms | 67.8% | 65.7 px | 27.9 px | 92.4 px |
| 20 fr | 350 ms | 67.8% | 65.7 px | −80.9 px | 144.4 px |
| 24 fr | 350 ms | 67.8% | 65.7 px | −64.2 px | 208.0 px |
Three ceilings, none of them a matter of taste:
-
Nothing left to win. The clear rate is 96% of its final value by six frames. Everything from 6 to 24 is worth 2.5 points.
-
The lie becomes visible. The character is 24 px tall. At five frames the deepest thin-air takeoff anyone took in 5,000 attempts was 7.2 px — under a third of a body, and nobody can see it. At ten it is 32.5 px, taller than the player, and the game is now visibly letting you jump off nothing.
-
A hard physical wall at 18.4 frames.
reach boughtgoes negative at 20: a jump that started that far down is spending its whole apex climbing back to the floor it left. Pastτ_max = v₀ / √(g_up · g_down) = 700 / √(2000 · 2600) = 307 ms = 18.4 framesthe apex no longer reaches the floor at all, so a longer window cannot be used by anybody — window A stops at 350 ms and stays there however far the dial goes.
The window that games converged on by feel is sitting in the gap between the first ceiling and the second. It is the largest window that is still invisible.
The buffer has no second ceiling, which is why it can be generous
| buffer | window B | A | B | C | course |
|---|---|---|---|---|---|
| 0 fr | 183 ms | 79.8% | 42.5% | 81.4% | 27.8% |
| 2 fr | 217 ms | 79.8% | 71.2% | 81.0% | 45.4% |
| 4 fr | 250 ms | 79.8% | 90.2% | 80.7% | 57.9% |
| 6 fr | 283 ms | 79.8% | 97.4% | 80.3% | 63.1% |
| 8 fr | 317 ms | 79.8% | 99.7% | 80.3% | 64.3% |
| 10 fr | 350 ms | 79.8% | 100.0% | 80.3% | 64.5% |
| 14 fr | 417 ms | 79.8% | 100.0% | 80.3% | 64.5% |
A buffered jump fires on the frame you land, from the floor, at full height. It is indistinguishable from a jump you made on purpose, so there is no drop to see and nothing to give it away. Column A is 79.8% in all seven rows. The buffer saturates at 10 frames and simply stops doing anything, which is the only reason to stop raising it — not a cost, an absence of further benefit.
Coyote time has a visible price and buffering does not. That asymmetry, not a difference in importance, is why one of these numbers is conventionally about five frames and the other can be twice that.
Whose run was it
Of 12,636 runs cleared with the default 5 and 6:
- 10.5% still clear when the identical press steps are fed to a controller with the timers at zero. The other 89.5% of winning runs were not won by the player’s hands.
- 52.6% of the jumps in a winning run happened at a moment when the floor was not there (33.8%) or when the press had already been made and expired (18.9%). Just over half of every successful run is jumps the player did not make.
That is what the replay without button is: the recording is a list of step indices, the simulation is deterministic at a fixed 1/60 s step, so the same input can be run through a different controller and compared frame for frame.
Reading the drawing
Because the runner never changes speed, x = v·t exactly, so a window in
milliseconds is a strip of the course and can be drawn 1:1 along the path the
player is on:
- teal — takeoffs the physics allows by itself.
- amber — bought by coyote time. It hangs off the end of the floor and curves downward, because that is where the player actually is during it.
- violet — bought by the buffer. It hangs backwards up the fall, before the floor arrives.
A jump taken on the amber draws a dashed line down to the surface it pretended to be standing on, labelled with the distance. On the default settings that label almost never passes 7 px.
Reuse
src/coyote.mjs has no DOM in it. createRun/stepRun is the controller and
its course; replay(presses, tuning) runs a recording through any tuning;
windowFor, measure and perJump are the measurement harness. The forgiveness
itself is eight lines in stepRun and would drop into any fixed-step character
controller — the two timers, the <=, and setting both to Infinity on the
frame you jump so one press cannot buy two jumps.
What is deliberately not in here: variable jump height, a fall-speed cap that matters, air control, corner correction. A fixed arc is what makes the windows scannable and the claims checkable. Add any of them and the numbers above stop being about the two timers.
scripts/measure.mjs regenerates every table on this page.
scripts/screenshot-demo.mjs drives the demo in a real browser for the shots
and doubles as the smoke test — it fails if the page and a cold simulation
disagree about the same attempt, or if the buffer ever moves a ledge jump.






