Two keys, no flags

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.

1. The store is two numbers; the world is a function of them

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.

Two saved keys, and the five things derived from them each frame A box labelled ship.save holds stage and flames. Five arrows lead from it to derived facts recomputed every frame: the lantern tint, the roost placement gate, the open grave, the next text id, and whether the reward is owed. A sixth box, seen.offering, sits apart: it is the one piece of live state not derived from the store. ship.save stage 0–4 flames 0–3 the only thing that persists his tint — dark at 0 flames, warmest at 3 which roost hangs — flames + 1 == index the grave is open — stage == 2 his next text id — base + f(stage, flames) he owes you one — stage == 3 recomputed every frame, never stored seen.offering live, not saved — the one exception, §3
Nothing on the right is written down. That is why quest reset is quest.clear() and nothing else: put the two numbers back and the world unwinds itself on the frames that follow.

2. Step 14: the reset, and the second Piece of Heart

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.

Step 14's five parts as a timeline of the two keys and the heart count A timeline in five parts. Part A earns a Piece of Heart, taking stage from 3 to 4 and the cumulative count from N to N+1. Part B resets, putting stage and flames to 0. Part C shows the heart count is unchanged by the reset. Part D crosses a doorway and he is dark again. Part E earns a second Piece of Heart, taking the count to N+2. A · earn it B · quest reset C · vanilla D · doorway E · earn again stage 3 → 4 stage 4 → 0 flames 3 → 0 stage 0 stage 3 → 4 a Piece of Heart,pressing nothing every declared keyback to its default count UNCHANGED hearts, flags, health too he is dark again,four pages + the choice and it pays again no paid flag to stop it 36 → 37, and 37 37 → 38
Part C is the assertion the step exists for. Part E is the one nobody had run: “do it again if you like” is literal.

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:

framestateluminance
step14_lit_before_reset.pngstage 4, 3 flames50.7
step14_dark_after_reset.pngstage 0, 0 flames, after the doorway29.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.

3. Step 23: the one place that is not derived — the claim

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.

Two Naturals, one claim, one Piece of Heart Two Poe instances are each spoken to and each arm an offer. A shared field called seen.offering holds one serial. The first instance's serial no longer matches, so it disarms and logs that it stops holding it out. Link then stands in range of each in turn: the disarmed one hands over nothing and the claim holder hands over one Piece of Heart. the placed Poe (660, 181, 20) · serial n spoken to first → arms the console’s Poe (660, 181, 300) · serial n+1 spoken to second → arms seen.offering one serial, shared by every instance serial no longer matches “stops holding it out” holds the claim stand in range → nothing stand in range → ONE piece
Measured, with Link walked into the 25-unit offer range of each in turn for 120 frames with nothing held: the disarmed instance hands over nothing and the claim holder pays once. BREAK_STEP23 removes the serial test, and both of them pay.

4. The sentence that names the wrong mechanism

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.

Two routes to one Queen: the doorway, and the adoption Top row: spawning a Hive Queen then typing quest stage 2 places nothing, so the grave only appears at the next room load, and that room load destroys the spawned Queen before the placed grave wakes a fresh one. Bottom row: spawning an open grave beside a standing Queen reaches the adoption in maw_wake, which counts the Queens already present and spawns none. WHAT THE DOC SAID TO TYPE — one Queen, but not by adoption spawn En_HiveQueen 0 1 Queen up quest stage 2 places nothing the doorway her instance is destroyed grave wakes a FRESH one WHERE THE ADOPTION ACTUALLY IS — the next line down the list a Queen already up from the placed grave, at stage 2 spawn En_OpenGrave 0 a second hole, beside the first maw_wake counts first “so none is spawned” — 2 graves, 1 Queen THE CONSOLE CHANGES THE QUEST; THE DOORWAY CHANGES THE WORLD
The console writes the store and places nothing. A 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.

5. Two traps this cost, both about pressing A

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.

What these two steps did not verify

“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.


Where the number stands

./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.