172 lines
11 KiB
Markdown
172 lines
11 KiB
Markdown
# Room escape: straighter runs and boundary rewards
|
|
|
|
Decided 2026-09-16 (the operator): the fly struggles with rooms. Two levers, both honest under the doctrine
|
|
(buttons always the fly's; readout fixed and global; rewards are design):
|
|
|
|
## 1. Straighter runs (readout)
|
|
|
|
The Game Boy preset commits to a direction for 400 ms with hysteresis 1.15 and fatigue 0.08/0.8.
|
|
One overworld step is about 270 ms, so runs are one to two steps and the walker jitters. Raise the
|
|
direction hold and decision period together (candidates 600, 800, 1000 ms), keep hysteresis, and
|
|
lower fatigue gain so a run can persist (0.04). Choose by measurement, then bake the value into the
|
|
preset as `gameboyDecoderConfig()` (TS and Rust twins, golden regenerated). Decoder version note
|
|
and the compatibility string are unaffected (readout state is not in the neural checkpoint) but
|
|
the readout doc records the change and the date.
|
|
|
|
Measurement: the random-walker harness (`services/flysim/crates/flysim/examples/random_walker.rs`
|
|
pattern, on the Pokémon ROM) started from the archived bedroom snapshot and from the house 1F
|
|
snapshot, 20 seeded runs per hold value, 10 brain minutes each, unthrottled. Report median time to
|
|
leave the map and the fraction that leave at all. Then confirm with the real brain for the chosen
|
|
value (5 runs) so the number is not a random-walker artefact.
|
|
|
|
## 2. Boundary rewards (adapter)
|
|
|
|
Pay a small bonus the first time per map the fly stands on a tile adjacent to a warp (door, stairs,
|
|
map edge exit) and again the first time it stands on the warp tile itself. Warps come from the
|
|
current map's warp table in WRAM (`wNumberOfWarps`, `wWarpEntries` at the pinned pokered commit;
|
|
verify names and layout: y, x, destination warp id, destination map). Map-edge exits are the
|
|
connection tiles (`wMapConnections`); pay the same for the first step onto a route boundary tile.
|
|
Values: adjacent 0.05, on-warp 0.10, capped once per (map, warp) for the lifetime ledger so
|
|
oscillating cannot farm. Kind `boundary`, feed reward kind mapping `explore` (no new feed kind),
|
|
label "found an exit". Adapter version `pokered-unique8-v5`; old checkpoints refused as before,
|
|
and the deploy-time compatibility check archives state.
|
|
|
|
Doctrine note for the honesty panel and README: the reward catalog now includes exits; still no
|
|
button path, still no map knowledge in the readout.
|
|
|
|
|
|
## 3. v0.1.1: the ground floor, diagnosed from the live fly
|
|
|
|
Decided 2026-09-16 (the operator), after the release box (the release container, `twitch.tv/<twitch-channel>`, v0.1.0)
|
|
spent forty minutes on rung 2 with the whole of section 2's boundary reward already collected:
|
|
"rework reward and/or movement and push out 0.1.1".
|
|
|
|
Section 1's measurement was a random walker from a freshly booted cartridge. That instrument cannot
|
|
see this bug, and said so at the time: *every* candidate left both rooms in every one of 480 runs,
|
|
so the leave fraction saturated and only the time to leave could be compared. It also measured the
|
|
wrong thing, which nobody noticed: on the ground floor "left the map" is the staircase, which is one
|
|
step and lands in the other room of the same house.
|
|
|
|
The v0.1.1 measurement therefore starts from the live fly instead — generation 525 of the release
|
|
run, pulled read-only off the release container — through `FLY_ESCAPE_CHECKPOINT` in the same harness, and reports
|
|
leaving the *house*. That state carries what a fresh boot does not: 43 brain minutes of plasticity,
|
|
a calibrated baseline the network has since drifted away from, and a reward ledger with every payout
|
|
in the house already spent. `infra/docs/room-escape.md` holds the numbers.
|
|
|
|
### What the room actually is
|
|
|
|
`survey` in the harness walks map 37 with real button presses on throwaway emulators. Red's ground
|
|
floor has **48 reachable tiles**, and exactly **six presses leave it**:
|
|
|
|
| from | press | to |
|
|
| --- | --- | --- |
|
|
| (6, 1) | right | map 38, the bedroom |
|
|
| (7, 2) | up | map 38, the bedroom |
|
|
| (2, 6) | down | map 0, Pallet Town |
|
|
| (3, 6) | down | map 0, Pallet Town |
|
|
| (2, 7) | down | map 0, Pallet Town |
|
|
| (3, 7) | down | map 0, Pallet Town |
|
|
|
|
Two facts follow that the design did not have when section 2 was written, and both matter more than
|
|
anything in it:
|
|
|
|
- **Leaving the map is not leaving the house.** Two of the six exits are the staircase, which warps
|
|
the instant it is stepped on and lands the fly in a room it has also exhausted.
|
|
- **The front door needs DOWN, and only DOWN.** The two doormats are on the bottom row and the two
|
|
tiles above them are walkable, so four tiles are one press from outside; on every one of them that
|
|
press is `down`. A sideways step onto a mat does nothing, which is why the live ledger holds
|
|
`boundary:37:7:2:on` and `boundary:37:7:3:on` — paid 0.10 each, at 355 s — from a fly that never
|
|
went outside.
|
|
|
|
### The four hypotheses, answered
|
|
|
|
- **(a) A persistent rate bias makes one direction win almost always, and fatigue 0.04 lets it
|
|
stick.** Half right, and the half that is wrong matters. There *is* a persistent bias — `down`
|
|
won 14 to 18% of decisions against `left`'s 32 to 34%, run after run — but no direction wins
|
|
almost always, because the bias is small and fatigue does rotate the winner every four decisions.
|
|
The bias is a standing preference order, not a lock. What made it fatal is that the direction at
|
|
the bottom of the order is the only one that opens the door.
|
|
- **(b) The door needs DOWN from the tile above the mat while the fly's DOWN drive is weak.** Yes,
|
|
and it is the proximate cause. Four tiles, one press, and that press is the least likely of the
|
|
four. Over fifteen ten-minute runs the fly spent 123 frames of 537,000 — 0.02% of its life — on
|
|
the two tiles above the mats.
|
|
- **(c) Hysteresis 1.15 plus long holds create wall-hugging loops.** Yes, and it is the biggest
|
|
number in the report: nearly two of every three 800 ms holds moved the player not one tile, for up
|
|
to 34 seconds at a stretch. The lock is arithmetic — the four direction scores sit inside a 9%
|
|
spread and a challenger needs 15% — so the winner could only ever be changed by habituation. The
|
|
fly orbited the walls, and the staircase is on that orbit while the front door is not.
|
|
- **(d) The stall rollback resets to the same room and wastes attempts.** Not what happened. The
|
|
ratchet fired **zero** recoveries in 43 live brain minutes and zero in 150 measured ones, with 0
|
|
of 3 attempts spent at rank 2. The stall window never matures: `Ratchet::observe` pushes
|
|
`lastProgress` forward on any unsafe stretch longer than `unsafeResetMs` (1 s), and a fly walking
|
|
and bumping produces those constantly. The objection is still true in principle — the archived
|
|
snapshot for rank 2 *is* a ground-floor state — but it is not this bug and nothing here changes
|
|
it. Recorded as a known limitation.
|
|
|
|
### The fix
|
|
|
|
Three numbers in the Game Boy preset, all readout, no reward rule and no button path:
|
|
|
|
| | v0.1.0 | v0.1.1 |
|
|
| --- | ---: | ---: |
|
|
| `holdMs` = `decisionMs` | 800 | 800 |
|
|
| `fatigueGain` | 0.04 | **0.08** |
|
|
| `hysteresis` | 1.15 | **1.05** |
|
|
| `blockedFatigue` / `blockedMs` | off | **0.35 / 800** |
|
|
|
|
Out of the house within ten brain minutes, fifteen runs each from generation 525: v0.1.0 got 1 out,
|
|
this gets 14, and 13 of those inside five minutes against 0. `infra/docs/room-escape.md` has the
|
|
candidate table, including the two that were tried and rejected (hysteresis 1.00, and hold 600) and
|
|
the interaction that decides the third number: the blocked-direction cooldown made things *worse* at
|
|
hysteresis 1.15 and is decisive at 1.05.
|
|
|
|
The blocked-direction cooldown is the one new mechanism, and it is deliberately the narrowest input
|
|
that fixes a wall bump: the sim loop tells the readout the name of a direction it has watched
|
|
produce no movement for a whole hold, and the readout habituates that channel. It says "that button
|
|
did nothing" — not where the fly is, not where the door is, not which button would have worked.
|
|
`docs/readout.md` specifies it and `docs/rewards-learning.md` discloses it on the honesty panel.
|
|
|
|
### What was not changed, and why
|
|
|
|
**The reward side.** Section 2's `boundary` rule pays once per exit per lifetime, and the design's
|
|
own v0.1.1 candidate was a small decaying bonus for each new visit to a warp-adjacent tile after
|
|
60 s. It was not needed and, on these numbers, would not have worked: the fly spent 0.02% of its
|
|
frames on a door-approach tile, so a repeat bonus would have paid at most once or twice in a
|
|
ten-minute run — too sparse for the eligibility traces to build anything from, and the runs that
|
|
most needed help never got near the door at all. The readout change fixes what was broken, and a
|
|
reward gradient is worth reconsidering now that the fly reliably reaches the tiles it could be paid
|
|
on. `REWARD_ADAPTER` stays at `pokered-unique8-v5` and `STATE_VERSION` at 4.
|
|
|
|
For the deploy: because readout state is not part of the neural checkpoint and the adapter version
|
|
is untouched, **the compatibility string does not move**. v0.1.1 restores the live fly's generation
|
|
525 and simply decodes on the new numbers. The run does not reset and the ladder does not
|
|
rebaseline.
|
|
|
|
**The direction-run cap.** The design listed "after N consecutive holds of the same direction, that
|
|
direction's fatigue jumps" as a candidate. It is already what `fatigueGain` does, one linear step at
|
|
a time instead of one jump, and unlike a counter it needs no new field in `DecoderState` and no
|
|
version bump. Raising the gain from 0.04 to 0.08 halves N, which is the same lever with none of the
|
|
cost.
|
|
|
|
**Recalibration.** `calibrate()` runs once, at warm-up, so the entire 9% spread between the four
|
|
direction scores is drift since then, frozen in. Re-tying them periodically would dissolve the
|
|
preference order at its root, but running once is what makes the readout *fixed*
|
|
(`docs/readout.md`, "The readout is fixed") — a doctrine commitment, not a tuning knob. Left for
|
|
The operator; `docs/limitations.md` records it.
|
|
|
|
## Verification
|
|
|
|
Synthetic WRAM traces: warp table parsing, adjacency, once-per-warp, edge exits, no payout inside
|
|
a map already baselined; ROM-gated: from the bedroom archive, the first payouts are boundary events
|
|
near the stairs. Random-walker report committed under `infra/docs/room-escape.md` with the chosen
|
|
hold value.
|
|
|
|
For section 3: the blocked-direction cooldown has unit tests on both decoder twins (one report
|
|
unseats an incumbent that habituation would have taken three more decisions to shift; repeated
|
|
reports inside one hold are one penalty; a report never lowers fatigue and is capped at 1; a report
|
|
is ignored when the rule is off or names a channel outside the group), and `GameAdapter::location`
|
|
has one on the adapter (the same tile in two rooms is two locations, which is what stops a warp
|
|
reading as standing still). The preset's own numbers are pinned separately, in
|
|
`flybrain-core/tests/decoder.rs` and `packages/brain/tests/readout.test.ts`, so a preset edit fails
|
|
a test. The live-state measurement itself is not a test: it needs a checkpoint that is not in the
|
|
repository and a quarter of an hour, and it is reproduced by hand from `infra/docs/room-escape.md`.
|