Playtest steps 14 and 23 of Ship of Harkinian’s Lua layer — what quest reset
can safely do, why two Poes cannot pay you twice, and the one sentence in the document that names the wrong
mechanism for the right answer.
2026-09-21 · headway:personal/timber-panic-couch
The Third Brother quest keeps its entire state in two saved keys — stage
(0–4) and flames (0–3) — and derives everything else from them, every frame. No event
flag, no scene state, no “has paid” boolean. That single decision is what makes
quest reset four characters long and safe to type, and it is what the last two unwritten steps of
docs/PLAYTEST.md turn out to be about. Writing them moved
./dev playtest --list from 21 of 23 written steps to 23 of 23.
Everything a player can see about this quest — whether a Poe is lit, whether a grave is open in the terrace,
which text box he opens, whether he owes you a Piece of Heart — is recomputed from those two keys rather than
stored beside them. ship.save.declare { stage = STAGE_MET, flames = 0 } is the whole of the
persistent state.
quest reset is
quest.clear() and nothing else: put the two numbers back and the world unwinds itself on the
frames that follow.docs/PLAYTEST.md step 14 says the reset puts every key back “without touching the save
file… nothing vanilla is undone, because this quest writes nothing vanilla: no event flag, no scene
state, no item beyond the one it gave you.” That claim cannot be asserted against a quest walked to
stage 4 by typing quest stage 4 — such a quest has never written a vanilla byte, so
“nothing was undone” is true of a reset that undid everything there was.
So the step earns the Piece of Heart first, the way step 12 does: stage 3, three flames, the conversation from outside the offer’s 40-unit range, then walk in and press nothing. Only then does it reset on top of a real vanilla write.
Part E follows from §1 with no room for argument. The exactly-once property step 13 is entirely about
— he must never offer a second Piece of Heart — is derived from stage == 4 and from
nothing else. There is no paid flag, so quest reset is the one thing in the game that can make him
owe it again, and the second piece is handed over exactly like the first. That is the design working, not a
hole in it; but it is worth knowing before typing it on a file you care about, so it is now in the doc.
“He is dark again” is a comparison rather than an adjective. Both frames are taken from the same
fixed south-west vantage — sw_vantage takes a constant world point, so two captures share a camera
exactly — and the mean luminance of the rectangle he stands in is read off each:
| frame | state | luminance |
|---|---|---|
| step14_lit_before_reset.png | stage 4, 3 flames | 50.7 |
| step14_dark_after_reset.png | stage 0, 0 flames, after the doorway | 29.4 |
The assertion is only dark < lit, with no threshold on either number, and that is
deliberate. The lit value is not stable and is not supposed to be: the lantern breathes on a 3.7-second cycle
at three flames and the scene-wide fog he installs breathes with it, so the frame is a function of the phase
the capture landed on. Across four runs on this machine the lit number ranged 40.2 to 92.6
while the dark number sat at 29.1 to 29.4 — a Poe with no flames has nothing to breathe.
A threshold on either one would be a golden blessed on a single phase.
The abuse list is ten things to type and one bar for all of them: none of these should crash, strand
you, or pay you twice. Nine of the ten are answered by §1 alone — type a stage, the world recomputes
and catches up. quest stage 4 at a Poe holding the Piece of Heart out prints
the offer is withdrawn — the stage is 4 (done), not 3 and hands over nothing;
quest reset mid-fight closes every open grave on the next frames and leaves the Hive Queen
standing, because the grave script owns her arrival and not her life.
The tenth is the exception. spawn En_ThirdBrother 0 beside the placed one gives you two Poes who
can each be spoken to, and an offer is not derived from the stage alone: it is live per-instance state.
One shared table field, seen.offering, holds a single serial across every instance of the script,
and the instance whose serial does not match it disarms on its next update.
BREAK_STEP23 removes
the serial test, and both of them pay.Step 23’s list contained spawn En_HiveQueen 0 then quest stage 2 — the grave adopts her, it does
not add a second. You do end up with one Queen. The reason given is not why.
ScriptedPlace declaration becomes an
Actor_Spawn only at a room load — and a room load clears what was standing, including the Queen the
console put there. The step asserts the outcome (one Queen, one grave) and reports that the route is
the doorway, with the adoption asserted separately where it really lives.A page that is still typing eats the press. next_page presses A and waits for
the message’s draw position to go backwards — what a fresh page looks like. An A pressed while
the typewriter is still running fast-forwards the current page instead of turning it, so a bare
while d.next_page(): loop presses for its entire timeout and then reports a page that never turned.
Measured on the stage-3 box: 58 of 79 characters drawn, six seconds of pressing, same page.
The fix is to settle each page before turning it.
A choice page must not be turned at all. A on a choice takes the choice. The
four-page meeting is text id 0xE000 for three pages and then 0xE001, and a loop that
walks to the end answers the question it was only supposed to reach.
And one about names: talk_to takes an actor name, and find_actor returns
probe’s first actor with it — probe’s order is the engine’s actor list, not placement order.
With a second Natural spawned at z = 300, probe listed the spawned one first. A step whose
subject is two instances cannot address either of them by name; it has to name the spot.
“Without touching the save file.” ./dev drive boots on the debug
save (slot 255) and never writes a file, so every number step 14 asserts is read out of the live save
context and there is no file on disc to diff. Step 18 is the only step in the suite that makes a real
one; a reset-across-a-real-save check belongs beside it and does not exist.
The randomizer line is a repeat. docs/PLAYTEST.md puts the seed in step 23, and
step 23 runs it — but _the_randomizer_seed was written for step 13 and runs there too, at the same
fixed seed against the same quest state. A full ./dev playtest now spends its 120-second budget on
it twice for one answer. It blocks nothing, so it is not a card; it is written down here.
The lit/dark comparison is in pixels. The rectangle Natural is measured in is the one step 2 uses, and it is the one thing in this harness that is not an engine unit — the window manager picks the window size. It is only ever used to compare two frames from the same run and the same fixed camera, never against a number written down on another day.
./dev playtest --list | grep -c '\[written'
21 → 23 of 23
Every step of docs/PLAYTEST.md now has an implementation. Two of them stand FAIL
on real defects they found rather than on being unwritten — step 9’s tear sitting 11.45 units under the
stone apron it is drawn in, and step 20’s fourteen text lines wider than the box. Those are the quest’s
bugs, and they are what the suite is for.
Steps 14 and 23 each PASS attached and on a fresh cold boot, and each has a live break that
bites under --sabotage: step 14’s resets only the flames and leaves the stage, step 23’s
lets every Natural keep the claim so two of them pay.