The jump that measurement corrected
The jump I derived on paper did not match the jump the engine produced. The five-tile teaching gap did not require the dash it was meant to teach. Two forgiving recovery platforms formed a staircase that erased the lesson entirely.
Those three failures surfaced only when I simulated the real tick order against the authored level. The research post had ended before any movement or level values were chosen; that smallest complete version has now shipped as Skyline Run.
The lesson is simple: in a discrete game, measurement is part of design. Arithmetic can propose a rule, but only the running system can show whether the rule is true.
I own and build this site and the AI Maker Lab channel — this is a build log, not an independent review.
The checklist became an executable contract
The thirteen questions from A1 to C5 stopped being prompts and became a design record. A1 reduced the game to one sentence: reach the rooftop exit of one neon district by running, jumping, and dashing; a hazard or fall returns the player to the latest checkpoint. Precision platforming owns the promise, so exact movement, attributable failure, and fast retry take priority.
A2 fixed execution at 60 Hz with DT = 1/60. The engine is pure and DOM-free: step(state, input, level) returns a new state and never mutates its argument. A3 and A4 became title → playing ⇄ paused → complete, with death kept inside play as an 18-tick input lock followed by checkpoint respawn rather than promoted into a separate scene.
B1 put position, velocity, contact, ability counters, SP, checkpoint progress, deaths, ticks, previous input, camera state, and announcements in one authoritative state. B2 and B3 fixed axis-separated collision with an axis-aligned bounding box (AABB)—a collision box that never rotates—plus one-ways that block only while falling from above and trigger precedence of goal, then checkpoint, then hazard. Complete and dead states lock input; retry preserves time, deaths, and the latest checkpoint.
B4 made input a measured contract. Jump press, hold, and release have distinct meanings. Coyote time—a few frames of forgiveness after leaving a ledge—and the jump buffer are stored as 6- and 7-tick counters, but their effective windows are both 5 ticks under the implemented order: presses 1–5 ticks across the boundary fire, while 6 or more do not. Swink supplied the language of control sensation, Pittman the designer-readable movement targets, Thorson the forgiveness mechanisms, and Keren the camera mechanisms coordinated with the jump.
The complete thirteen-question record, including the large C5 boundary, is in the Skyline Run design record. The point here is not to repeat it. It is to show where those recorded answers were wrong before measurement corrected them.
The measured jump set the real boundaries
The game runs on a 640×360 canvas with 16 px tiles and a 12×20 px player AABB. These are the shipped tuning values, not starting estimates.
The story turns on three of them: JUMP_VELOCITY sets the launch, GRAVITY_UP shapes the measured apex, and RUN_SPEED decides whether a gap teaches the dash.
| Constant | Value | Meaning |
|---|---|---|
RUN_SPEED | 130 | Maximum horizontal speed in px/s. |
ACCEL_GROUND | 1400 | Ground acceleration in px/s². |
DECEL_GROUND | 1800 | Ground deceleration in px/s². |
ACCEL_AIR | 1100 | Air acceleration and deceleration in px/s². |
JUMP_VELOCITY | -500 | Upward velocity when a jump fires, in px/s. |
GRAVITY_UP | 2080 | Rising gravity in px/s². |
GRAVITY_DOWN | 2600 | Falling gravity in px/s². |
APEX_THRESHOLD | 40 | Speed threshold for the held-jump apex treatment. |
APEX_GRAVITY_FACTOR | 0.5 | Gravity multiplier near the apex while jump is held. |
JUMP_CUT_FACTOR | 0.45 | Rising velocity multiplier on release. |
MAX_FALL | 560 | Maximum falling speed in px/s. |
COYOTE_TICKS | 6 | Stored coyote counter. |
BUFFER_TICKS | 7 | Stored jump-buffer counter. |
DASH_SPEED | 320 | Fixed horizontal dash speed in px/s. |
DASH_TICKS | 9 | Duration of a dash. |
DASH_COOLDOWN_TICKS | 12 | Cooldown after a dash ends. |
SP_REGEN_TICKS_RUN | 48 | Grounded running ticks required to restore one SP. |
SP_REGEN_TICKS_IDLE | 96 | Grounded idle ticks required to restore one SP. |
RESPAWN_TICKS | 18 | Dead-state ticks before respawn. |
CAM_LOOKAHEAD | 24 | Facing-direction camera offset in px. |
CAM_LERP_X | 8 | Horizontal smoothing coefficient. |
CAM_LERP_Y | 6 | Vertical smoothing coefficient. |
CAM_WINDOW_UP | 72 | Upward camera escape window in px. |
CAM_WINDOW_DOWN | 40 | Downward camera escape window in px. |
A full-hold jump reaches exactly 56.00 px, or 3.5 tiles, on tick 14 at 0.233 s. Total airtime is 0.467 s. Releasing after one tick produces a 16.56 px hop, about one tile.
| Jump held through tick | Measured apex |
|---|---|
1 | 16.56 px |
2 | 22.42 px |
3 | 27.72 px |
4 | 32.62 px |
5 | 37.02 px |
6 | 40.97 px |
The apex rises monotonically with every additional held tick in that range. A dash covers exactly 48 px, or 3 tiles, over exactly 9 ticks while vertical velocity remains pinned to zero.
Every gameplay timer is an integer tick counter rather than a float in seconds. That removes accumulation drift from the rules and turns each boundary into an exact assertion: one tick before, the event is available; one tick after, it is not. The page can still format ticks as time, but the engine never has to ask whether a nearly exhausted floating-point timer is effectively zero.
Where the arithmetic lied
The 4-versus-6 discrepancy belonged to the originally planned constants: JUMP_VELOCITY -350, GRAVITY_UP 1094, GRAVITY_DOWN 1750, and RUN_SPEED 140. The pre-build table said jump alone cleared at most 4 tiles, but simulating those same constants measured 6. The 12 px collider kept the player grounded until its left edge cleared the ledge, moving the real takeoff point 12 px later than the point-mass model assumed. Coyote time then added up to 5 more ticks before takeoff, worth about 11.7 px at 140 px/s. The table was internally inconsistent too: its own arithmetic already implied more than 4 tiles of reach, yet it labelled the result “at most 4 tiles,” even though 4 tiles is only 64 px.
A separate error concerned apex height, not horizontal reach. With the shipped JUMP_VELOCITY -500 and GRAVITY_UP 2080, the closed-form apex is 500² / (2 × 2080) = 60.10 px. Semi-implicit Euler integration—updating velocity first and then position once per fixed tick—measures exactly 56.00 px on tick 14, a loss of 4.10 px against the predicted v × dt / 2 = 500 / 120 = 4.17 px. That lower apex shortens airtime, so it cannot explain a larger-than-predicted horizontal envelope.
Neither error is obscure once written down. The mistake was using a continuous derivation and an internally inconsistent sketch to certify a discrete system whose input timing, collider, and collision order were not in the equation. I stopped treating either derivation as the final value and simulated the implemented tick order instead.
For a discrete simulation, the closed-form solution is a hypothesis, not a specification. It remains useful for choosing a starting region. The acceptance value must come from the system that will actually run.
The gap that taught nothing
The centre of the first room was a flat 5-tile, 80 px gap intended to say, “you need the dash here.” The engine’s reproducible boundary says otherwise: a flat 5-tile gap is crossable with a jump alone, while a flat 6-tile gap is not. At RUN_SPEED 130, the player travels 130 / 60 = 2.167 px horizontally per tick, so the old gate was decided inside a single tick of travel. Which coyote-time tick fired the jump, together with the approach’s sub-pixel phase—the fractional pixel remainder carried between frames—changed the answer.
The load-bearing detail is the player’s 12 px-wide box. The player remains grounded while the box’s left edge is still supported, so the real takeoff point is the platform’s right boundary. My first calculation measured from the wrong point. That error was small enough to hide inside a plausible sketch and large enough to invalidate the room.
A lesson decided by one frame of horizontal travel is an accident, not a design. The gap became 6 tiles, or 96 px, so jump alone reliably fails and the dash carries the player across. The number now says what the room says.
The first recovery design failed for the same reason at a larger scale. I added two one-way platforms so a missed dash would be forgiving. Together they formed a staircase around the gate, letting the player climb to the destination without performing the lesson. The level’s generosity deleted its own claim.
Those platforms became one recovery platform below the gap. The player can return to the takeoff side, but the destination is a 4-tile rise from that platform, higher than the 56 px apex, so recovery cannot become a shortcut.
The authoring envelope ruled out a ground-dash gate
Measuring the engine across landing heights produced the level-authoring envelope: the range of level geometry the measured jump can clear. Each value below is the maximum crossable open gap in tiles.
| Landing versus takeoff | Jump only | Jump + 1 air dash | Jump + 2 air dashes |
|---|---|---|---|
| Flat | 5 | 8 | 11 |
| 1 tile higher | 4 | 8 | 8 |
| 2 tiles higher | 4 | 7 | 7 |
| 1 tile lower | 5 | 9 | 12 |
| 2 tiles lower | 5 | 9 | 12 |
The table exposes a structural limit, not an engine bug. A grounded dash travels 3 tiles, while jump alone crosses a 5-tile flat gap. Any opening narrow enough for a grounded dash is already inside jump-only reach. A grounded dash can be expressive—a quicker rhythm or a cleaner line—but it can never be a strict geometry lock.
The dash can only be gated in the air. Even then, a second dash does not improve a raised landing: the 12-tick cooldown consumes the remaining airtime. That changes level authoring plainly. I cannot label a gap “ground-dash required” because I want that beat, and I cannot budget a second dash from SP alone. Every gate has to be selected from the measured row for its landing height and verified against the implemented cooldown.
Tests that hold a level to its promise
The most interesting checks are not tests of acceleration or gravity in isolation. They test whether district-01, a 200×30-tile level in four rooms, still teaches what it claims to teach.
level.spec.ts pins the complete standable-surface list and verifies that the spawn, every checkpoint, and the goal stand on ground. It checks each main-line hop against its dash budget, keeps both dash-gated gaps jump-uncrossable, proves the recovery platform cannot bypass its gate, and proves that neither recessed pit can soft-lock the player.
level.spec.ts now includes a committed replay test that pins completion at tick 1304, or 21.7 s, with 0 deaths and all five checkpoints activated, then verifies an identical replay from a fresh state. The committed engine, level, and route suites all pass, and bun run check is clean.
The browser provided the final check against the rendered route. A maximum jump alone ended in the teaching pit against its far wall; the player slid to the pit’s right wall, so that resting position measured the wall rather than jump reach. One air dash landed at column 26.26. The full 1304-tick runtime replay reached the “District cleared” overlay, whose statistics line showed 0:21 · 0 falls.
A level’s teaching claim is a testable contract. “This gap teaches the dash” can be decomposed into what cannot cross, what can cross, what happens after failure, and whether the recovery route preserves the lesson. Once those statements are executable, later tuning cannot silently turn the room back into a lie.
Measurement turned the level’s claims into facts
Measurement is a design instrument, not a QA step at the end. The ABC checklist told me which questions to ask and where to record the answers. Only simulation told me whether those answers were true under the real update, input, collider, and level rules.
The first playable slice is still only one level, one mechanic lesson, keyboard input plus on-screen touch controls for move, jump, dash, and pause on coarse-pointer devices, and programmatic graybox art. The deferred boundary remains large: the other movement abilities, combat and items, enemies, run and exploration structures, authored art, sound, gamepad input, persistence, creator tools, and adjustable challenge are not hiding behind the completion overlay. They have not been built.
A deterministic clear still does not prove that an uncoached player will read the lesson, find the recovery route, or enjoy the movement. That remains an open question, and the named next step is observed playtesting while the measurements stay in place to show what changes when players expose another false assumption.
What this slice did prove is narrower: under the real engine rules, the geometry no longer contradicts the lesson it is meant to teach. Next, I turn that deterministic engine into a training ground and ask, Can a deterministic game teach a network to run the rooftops?
Sources
- Swink, Steve; CRC Press. Game Feel: A Game Designer’s Guide to Virtual Sensation. First edition, copyright 2009. https://www.routledge.com/Game-Feel-A-Game-Designers-Guide-to-Virtual-Sensation/Swink/p/book/9780123743282. Accessed 2026-08-19.
- Pittman, Kyle; Game Developers Conference. “Math for Game Programmers: Building A Better Jump.” GDC 2016. https://gdcvault.com/play/1023559/Math-for-Game-Programmers-Building. Accessed 2026-08-19.
- Thorson, Maddy. “Celeste & Forgiveness.” Publication date not displayed. https://www.maddymakesgames.com/articles/celeste_and_forgiveness/index.html. Accessed 2026-08-19.
- Keren, Itay. “Scroll Back: The Theory and Practice of Cameras in Side-Scrollers.” May 11, 2015. https://www.gamedeveloper.com/design/scroll-back-the-theory-and-practice-of-cameras-in-side-scrollers. Accessed 2026-08-19.
I own and build this site and the AI Maker Lab channel — this is a build log, not an independent review.