workshop private

← all mechanics

Coyote

mechanics · created 2026-10-13

A one-button auto-runner where the only input is when, built to measure what the two famous platformer forgiveness timers are actually worth — coyote time moves a ledge jump from 39.5% to 79.9% and an input buffer moves it by exactly nothing, 89.5% of winning runs die when the same presses are replayed with the timers off, and a player with zero timing error clears the course 0% of the time.

game-feelplatformermovementsimulationcanvas

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.

jumpgapslack (reach − gap)the cue
A148 px16.2 px = 65 msthe first ledge
B110 px54.2 px = 217 mstouchdown on the stub
C150 px14.2 px = 57 msthe 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 / bufferA (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 / 05 / 6
0 ms0.0%100.0%
5 ms4.7%100.0%
15 ms14.5%99.3%
30 ms11.0%83.8%
45 ms6.9%63.0%
60 ms4.3%45.4%
80 ms2.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 / bufferABCcourse
0 fr / 0 fr39.5%41.8%37.9%6.6%
5 fr / 0 fr79.9%42.5%80.4%27.5%
0 fr / 6 fr39.5%97.1%38.3%15.3%
5 fr / 6 fr79.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.

coyotewindow Acleardeepest takeoff seenreach boughtfallen
0 fr50 ms15.4%0.0 px0.0 px0.0 px
3 fr100 ms51.8%2.2 px11.5 px3.3 px
4 fr117 ms59.0%4.3 px14.8 px5.8 px
5 fr133 ms63.1%7.2 px18.0 px9.0 px
6 fr150 ms65.3%10.8 px20.8 px13.0 px
8 fr183 ms66.3%20.2 px25.7 px23.1 px
10 fr217 ms66.5%32.5 px29.4 px36.1 px
12 fr250 ms66.6%47.7 px31.5 px52.0 px
16 fr317 ms67.8%65.7 px27.9 px92.4 px
20 fr350 ms67.8%65.7 px−80.9 px144.4 px
24 fr350 ms67.8%65.7 px−64.2 px208.0 px

Three ceilings, none of them a matter of taste:

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

bufferwindow BABCcourse
0 fr183 ms79.8%42.5%81.4%27.8%
2 fr217 ms79.8%71.2%81.0%45.4%
4 fr250 ms79.8%90.2%80.7%57.9%
6 fr283 ms79.8%97.4%80.3%63.1%
8 fr317 ms79.8%99.7%80.3%64.3%
10 fr350 ms79.8%100.0%80.3%64.5%
14 fr417 ms79.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:

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:

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.