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.
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.
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.
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.
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:
| direction | rungs, outside → in | the wall |
|---|---|---|
| dy > 0, the ceiling | 75.0 · 60.0 · 56.0 · 55.3 · 55.0 · 54.9 caught | 55.0 |
| dy < 0, the floor | −75.0 · −55.3 · −55.0 caught | 55.0 |
| dxz, the waist | 45.0 · 34.0 · 32.0 · 31.9 caught | 32.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.
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.
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.
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 Link | verdict |
|---|---|
measure_catch_box.py, its own ladder | caught |
measure_catch_box.py --func edge, six fresh releases | caught 6/6 |
playtest --steps 7,17, the same offset at the same roost | not caught |
playtest --sabotage --steps 7,17, same again | caught |
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.
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.
| roost | updates, as written | re-measured | from the sac, as written | re-measured |
|---|---|---|---|---|
| A, behind the stones | 40–74, med 62 | 31–67, med 64 | 276–486 | 253–481 |
| B, the top terrace | 16–39, med 20 | 18–39, med 21 | 132–302 — or “about 160” | 138–299 |
| C, the Kakariko ledge | 30–42, med 34 | 31–37, med 33 | 145–272 | 133–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.
+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.
--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.
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.