← Lab notebook

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.

Skyline Run's white player character crosses a six-tile rooftop gap with a cyan air-dash trail while measured jump and level values frame the scene.
The first playable slice is small enough that every movement value can be connected to a specific lesson in the level.

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.

ConstantValueMeaning
RUN_SPEED130Maximum horizontal speed in px/s.
ACCEL_GROUND1400Ground acceleration in px/s².
DECEL_GROUND1800Ground deceleration in px/s².
ACCEL_AIR1100Air acceleration and deceleration in px/s².
JUMP_VELOCITY-500Upward velocity when a jump fires, in px/s.
GRAVITY_UP2080Rising gravity in px/s².
GRAVITY_DOWN2600Falling gravity in px/s².
APEX_THRESHOLD40Speed threshold for the held-jump apex treatment.
APEX_GRAVITY_FACTOR0.5Gravity multiplier near the apex while jump is held.
JUMP_CUT_FACTOR0.45Rising velocity multiplier on release.
MAX_FALL560Maximum falling speed in px/s.
COYOTE_TICKS6Stored coyote counter.
BUFFER_TICKS7Stored jump-buffer counter.
DASH_SPEED320Fixed horizontal dash speed in px/s.
DASH_TICKS9Duration of a dash.
DASH_COOLDOWN_TICKS12Cooldown after a dash ends.
SP_REGEN_TICKS_RUN48Grounded running ticks required to restore one SP.
SP_REGEN_TICKS_IDLE96Grounded idle ticks required to restore one SP.
RESPAWN_TICKS18Dead-state ticks before respawn.
CAM_LOOKAHEAD24Facing-direction camera offset in px.
CAM_LERP_X8Horizontal smoothing coefficient.
CAM_LERP_Y6Vertical smoothing coefficient.
CAM_WINDOW_UP72Upward camera escape window in px.
CAM_WINDOW_DOWN40Downward 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 tickMeasured apex
116.56 px
222.42 px
327.72 px
432.62 px
537.02 px
640.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.

A diagram compares an analytically derived jump arc with the measured 60 Hz arc, marking the exact 56 pixel apex on tick 14 and the shorter one-tick-release hop.
The useful curve is the one produced by the engine's actual update order, including release, apex gravity, and collision.

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.

A measured diagram contrasts the accidental five-tile teaching gap with the corrected six-tile gap and highlights the one-tick boundary that made the old gate unreliable.
A teaching gate needs enough margin to survive the exact collider and timing rules, not merely enough to look convincing on a tile sketch.

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 takeoffJump onlyJump + 1 air dashJump + 2 air dashes
Flat5811
1 tile higher488
2 tiles higher477
1 tile lower5912
2 tiles lower5912

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.

An ability-envelope diagram maps maximum gap widths across flat, raised, and lowered landings for jump only, one air dash, and two air dashes.
The useful authoring tool is not one headline distance but a table that includes landing height, dash count, and the time consumed between abilities.

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

I own and build this site and the AI Maker Lab channel — this is a build log, not an independent review.

Keep reading