The rung and the drift

Ship of Harkinian’s Third Brother quest has a catch box — 32 units across, ±55 tall — and both numbers were picked blind. This is what happened when a live ladder was sent to measure them, and what a live ladder turns out not to be able to say.
2026-09-21 · headway:personal/rubber-awesome-share, verified

You catch the flame wisp by walking into it. There is no collider: the whole of catching is two comparisons against two constants, run before the flame moves each update. Which makes CATCH_XZ and CATCH_Y the kind of numbers a test can pin exactly — and makes the way they were pinned worth looking at, because the answer the card recorded is right and the sentence it recorded it in claims more than the instrument can deliver.

1. The box is two comparisons, and a chase never arrives at its edge

The wisp’s update tests the catch first, against distances Actor_UpdateAll has already filled in, and only then moves:

if (self.ticks > WISP_GRACE)
    and (self.xz_dist_to_player < CATCH_XZ)      -- 32.0
    and (math.abs(self.y_dist_to_player) < CATCH_Y) then   -- 55.0
    wisp_caught(self)

examples/third_brother/main.lua — wisp_update

A strict < on each, so the wall itself is outside the box. And that is the thing a chase cannot measure: a chase arrives from wherever the chase went. Over sixty live chases the flame was caught from all sorts of distances and not once from 54.9 units below Link on purpose. To ask where the edge is you have to stop chasing and start placing.

The catch box around Link Link stands at the centre. A dashed rectangle around him extends 32 units in the horizontal direction and 55 units above and below. A flame inside the box is caught; two flames outside it, one above and one to the side, are not. Link CATCH_Y 55.0 above 55.0 below CATCH_XZ 32.0 across caught 75 above — no catch 45 across — no catch
Two comparisons and no collider — which is the quest’s own decision, and the right one: an AC collider would mean swinging a sword at the thing you were told to walk into.

2. So the flame gets placed — and the flame is what gets moved, not Link

A Link teleported 40 units under a hovering flame arrives on the floor instead, because his position is swept by collision. The wisp has gravity = 0 and tests the box before it moves, so the honest direction is to move the flame. One probe reads Link’s position; the flame is written to that position plus the offset; the sim steps one frame; the box either closed or it did not.

It has to be held across two frames. Held for one, the ladder reports the rung before it — the whole first draft of this measurement came out shifted by one rung.

3. And an unwritten flame falls 0.675 units a frame

That is the number the two-frame rule exists for, and it had never been printed. Tracing the hold frame by frame at roost B, with Link standing at y 359.011:

+75.0 frame 1: wrote flame y 434.011 (link 359.011 +75.0); step 1 frame...
+75.0 frame 1: after the frame the flame is at y 433.336 -- it MOVED -0.675
               so the offset compared next frame is 74.325, not 75.0

scenarios/measure_catch_box.py --func trace

The flame is not where it was put by the time the next frame looks at it. Its velocity.y is its own — a wisp parked 55 units off its hover target ramps toward WISP_SINK/WISP_CLIMB, and 0.45 × the actor’s 1.5 scale is 0.675 a frame. Rewriting the position every frame is what makes the engine compare the offset that was asked for: 55.3 written → no catch54.9 written → caught on the same release, which is the proof that the write wins.

One rung of the ladder, frame by frame A timeline of four steps: probe reads Link's y; the flame is written to Link's y plus the offset; the sim steps one frame, during which the engine compares the written offset and the flame then drifts 0.675 units downward; the position is rewritten for the second frame. probe link y 359.011 write flame 359.011 + dy step 1 frame the engine compares |dy| < 55.0 the flame drifts −0.675 so rewrite it frame 2: write it again, or the box is asked about a position nobody chose
The drift is why a single-frame hold measures the previous rung, and why the offset has to be rewritten rather than set once.

4. Both walls, to a tenth

Rewriting every frame, the ladder comes out clean from the outside in. Roost B, because its terrace is walled in and its chase is the shortest of the three, so a release is cheap to stack probes on:

directionrungs, outside → inthe wall
dy > 0, the ceiling75.0 · 60.0 · 56.0 · 55.3 · 55.0 · 54.9 caught55.0
dy < 0, the floor−75.0 · −55.3 · −55.0 caught55.0
dxz, the waist45.0 · 34.0 · 32.0 · 31.9 caught32.0

Both constants are where they say they are. CATCH_Y is 55.0 and CATCH_XZ is 32.0, bracketed at a tenth, and docs/PLAYTEST.md’s “both numbers were picked blind” now has both of them read off the running game. That much of the card holds, and it is the answer it was asked for.

5. The rung that sits on the wall measures the instrument

Read rows one and two together. Probed at exactly +55.0 the flame was not caught; probed at exactly −55.0 it was. Off one comparison that is math.abs(y_dist_to_player) < CATCH_Y and cannot be asymmetric by design.

It is not a coin flip either. Probed repeatedly — misses stacked inside one release, each catch paid for with a fresh sac and a fresh flock — +55.0 caught 0/6−55.0 caught 6/6. A stable asymmetry in a symmetric comparison means the two sides are not being asked the same question. The only asymmetric term found is the harness’s own: probe prints the player’s position with %.3f (HarnessProbe.c:292), and the ladder builds its offset off that printed y, so “55 above” and “55 below” are not the same distance — they differ by whichever way that rounding went, up to 0.0005, in a fixed direction. That is a candidate, not a demonstration: it has the right size and the right sign structure and it was not isolated further, because the response either way is the same.

what that costs, and what it doesn’t

Nothing, against a rung 0.1 off the wall — a tenth is two hundred times the rounding. Everything, against a rung on it. So the wall is bracketed at 0.1 and never probed at 0.0, and ±55.0 is reported as a property of the harness rather than of the quest.

6. And the tenth still does not belong in the suite

The obvious next move was to promote the tight rungs into playtest step 7, so the number the step prints is the number it asserts. It was tried, and it failed — not the measurement, the promotion.

the same rung, +54.9 above Linkverdict
measure_catch_box.py, its own laddercaught
measure_catch_box.py --func edge, six fresh releasescaught 6/6
playtest --steps 7,17, the same offset at the same roostnot caught
playtest --sabotage --steps 7,17, same againcaught

Unanimous inside the scenario, and it flips between contexts. Link is standing in the same place either way — step 7 now prints the ladder’s rungs are offsets from Link at y 359.011, which is the scenario’s number to the millimetre, so his position is not the variable. What the variable is, this page does not know.

Which settles where each number lives. The step keeps the bracket it can carry — 45 in, 75 out, thirty units wide, green on every run there has ever been — and the tenth lives in a scenario that prints it and asserts nothing. The one thing that was actually wrong was the sentence in between: step 7 used to end by announcing “swept finer offline, 54 above catches and 55.3 does not, which is CATCH_Y to the unit”, in its own voice, having measured a 30-unit bracket. A step that prints a number it did not measure is how a constant moves to 40 and the sentence keeps printing.

7. What else the re-measurement moved

The sweep behind the card was re-run whole: fifteen more chases, interleaved A/B/C. The ordering reproduced — roost B is still the shortest chase of the three, which is the tiebreak two earlier sessions could not settle — and two of the ranges did not.

roostupdates, as writtenre-measuredfrom the sac, as writtenre-measured
A, behind the stones40–74, med 6231–67, med 64276–486253–481
B, the top terrace16–39, med 2018–39, med 21132–302 — or “about 160”138–299
C, the Kakariko ledge30–42, med 3431–37, med 33145–272133–204

Roost A’s floor of 40 was the low end of forty-five samples, not a wall. The verification sweep drew 31 and 41. Which is the case for step 7 asserting CHASE_MIN / CHASE_MAX — 20 and 200, the walls of judgement call #4’s own band — instead of the spread, and the median never moved more than two updates.

And roost B’s distance from the sac was carried in two irreconcilable forms from the same sweep. docs/PLAYTEST.md’s table had 132–302; three places in the harness — the sweep’s own docstring, step 17’s failure message, and the catch-box walk’s rationale for choosing roost B — had “about 160”, which is off by nearly half. The re-measurement got 299 and the table was right. The sweep prints the number; the three copies of it did not.

8. How it goes wrong, and where this page drifts

the cross-context flip is unexplained

+54.9 caught 6/6 in its own scenario and missed once inside step 7. Link’s y is identical in both (359.011), so the obvious cause is ruled out and no other was isolated. The tight rungs are therefore not in the suite — that is the response to not knowing, not a claim to have fixed it.

step 7’s break bit half the time — found here, fixed here

--sabotage breaks each step’s game on purpose and insists the step flips to fail. Step 7’s break set WISP_FLEE to 20.0 — faster than a child can run — and it bit three runs in six. Fleeing faster does not make a wisp uncatchable: WISP_LEASH turns the flame for home at 420 units and home is the sac Link is standing at, so a flame that sprints away at 20 a frame comes back through him at 20 a frame. The runs that bit came in at 268 and 298 updates against a CHASE_MAX of 200; the runs that did not came in at 36, 59, 94 — nowhere near the wall, so the shape is bimodal rather than borderline and tightening the band would not have fixed it.

Replaced, in the same commit, with WISP_GRACE = 10 → 400: the catch is refused while ticks <= WISP_GRACE and the flame only lives 300 updates, so no distance and no player can take it. 3/3 bit. The test for a candidate break is “can the game still reach the passing state by any route”, not “is this number worse” — and one run that bites looks exactly like a break that works.

the numbers here are one machine

Everything above is monad, turbo on, sim paused and stepped. The 0.675 drift follows from WISP_SINK × the actor’s scale and should hold anywhere; the chase medians moved twenty updates between sweeps on the same machine and should not be read as tight.

What the page simplifies: the box is drawn as a rectangle, and CATCH_XZ is a radius in xz — the real shape is a cylinder 64 units across and 110 tall, not a box. The ladder only ever probed one compass direction of the waist, so “32.0 across” is measured due east of Link and assumed round.


Sources: examples/third_brother/main.lua (wisp_update, CATCH_XZ, CATCH_Y, WISP_SINK, WISP_LEASH), tools/harness/playtest.py (_hold_the_flame_at, _walk_the_catch_box, CHASE_MIN/CHASE_MAX, BREAK_STEP7), tools/harness/scenarios/measure_catch_box.py (new: the ladders, --func wall, --func trace, --func edge), tools/harness/scenarios/measure_wisp_chase.py, HarnessProbe.c:292, docs/PLAYTEST.md steps 7, 8 and 17.