Merge fix/loop-20260922T0459: NEXT never on a pad with an input-accepting cursor; MOVE 1 is the main menu backstop; the bag is the fly's turn
This commit is contained in:
commit
4bd0a4b3fa
9 changed files with 720 additions and 67 deletions
|
|
@ -859,6 +859,61 @@ reproduction and the numbers; two macros, and section 12.2's one rule between th
|
||||||
|
|
||||||
The decoder, the reward catalog, the adapter version and the compatibility string are untouched.
|
The decoder, the reward catalog, the adapter version and the compatibility string are untouched.
|
||||||
|
|
||||||
|
### 12.10 No two buttons on a battle pad undo each other (2026-09-22, rung 9, seventy-one hours)
|
||||||
|
|
||||||
|
Thirty-five minutes after v0.4.2 deployed, the watchdog flagged the same rung again. Rank 9
|
||||||
|
(VIRIDIAN FOREST, next PEWTER CITY), seventy-one hours on it, and since the restart the macro
|
||||||
|
starts were `NEXT` **1264**, `BACK` **1241**, `THROW BALL` 5 and `GO WARP` 3 -- the event log
|
||||||
|
alternating `NEXT start/done, BACK start/done` every hold, on map 51.
|
||||||
|
|
||||||
|
Reproduced from the live checkpoint with the real cartridge and the real brain
|
||||||
|
(`infra/docs/macros-traps.md` has the numbers): twenty brain minutes, **71,673 frames of them all
|
||||||
|
in one battle**, 73 of 73 windows flagged, one distinct tile, and the two counts that name the
|
||||||
|
mechanism -- `BACK` **739 starts on the move list** and `NEXT` **739 on the top-level menu**.
|
||||||
|
|
||||||
|
- **The pair was split across two sub-states of one turn, which is why 12.9 did not catch it.**
|
||||||
|
`NEXT`'s script is one press of A. On a frame of battle text that A advances the text, which is
|
||||||
|
what the button is for. On the *top-level battle menu* the same A press **confirms whatever the
|
||||||
|
cursor is sitting on**, and the cursor sits on FIGHT, so `NEXT` opened the move list -- whose
|
||||||
|
`BACK` closed it again. Each button was legitimate where it stood: 12.9's rule is about `BACK`
|
||||||
|
and a list, and there *was* a list to leave. What is a trap is the **pair**: two buttons that
|
||||||
|
undo each other with nothing else changing, so the roll lands on one of them nearly every hold
|
||||||
|
and the turn never resolves. Section 12.2's rule, stated at the pad instead of at one macro:
|
||||||
|
**no pair of buttons on any battle pad may undo each other with nothing else changing.**
|
||||||
|
- **`NEXT` is the A that advances text, so it belongs only on a frame with no cursor accepting
|
||||||
|
input.** It is off every own-turn pad: the top-level menu, the move list, the party list and the
|
||||||
|
bag. The between-turns row keeps it alone, and the forced switch keeps it as row 8's backstop --
|
||||||
|
the one arm with a cursor and no `BACK` at all, because a forced switch cannot be cancelled.
|
||||||
|
- **`MOVE 1` is the backstop the top-level menu keeps.** `NEXT` was there for the turn where every
|
||||||
|
other button drops (row 7), and it was the wrong button for the job twice over: it does not end
|
||||||
|
a turn, and what it does instead is reopen the list `BACK` had just closed. `MOVE 1`'s
|
||||||
|
precondition over that menu is 12.8's -- "is there a move list to open", and FIGHT always opens,
|
||||||
|
because `CheckPlayerHasUsableMoves` sets Struggle without opening it -- so it is bound there
|
||||||
|
whatever the seam makes of `wBattleMon*`, and its script over that menu is "confirm FIGHT and
|
||||||
|
stop", which reads no move at all. A battler the seam cannot read is not a reason to take the
|
||||||
|
turn's one ending button away.
|
||||||
|
- **The bag is the fly's turn, and its pad is the bag's own answers.** `Battle::own_turn` answered
|
||||||
|
`false` for it, which was the last frame in the game with a cursor accepting input and no own
|
||||||
|
turn -- so it landed on the between-turns row, and that row's `NEXT` on an open bag is the A
|
||||||
|
press that *uses* whatever the cursor holds. The invariant is now whole: **a battle frame with a
|
||||||
|
cursor accepting input is the fly's turn**, the forced switch excepted because it has a pad of
|
||||||
|
its own. The bag's row is `ITEM` (use the thing) / `THROW BALL` / `BACK`; `CONFIRM` is gone from
|
||||||
|
it, because it was the same blind A press under another name, and both scripts already navigated
|
||||||
|
this list by reading its cursor (7.1 of `macros-wram.md`).
|
||||||
|
- **`wListMenuID` is not stale, which is why the bag reading stands.** It is zeroed by
|
||||||
|
`DisplayTextIDInit` at the start of every text display (`macros-wram.md`), so a battle's text
|
||||||
|
frames cannot inherit an `ITEMLISTMENU` from a bag the fly closed. Checked because a stale byte
|
||||||
|
there would have put the bag's pad on every frame of battle text.
|
||||||
|
- **What the harness now holds, rather than the pads alone.** The ROM-gated run from this
|
||||||
|
checkpoint asserts that no battle pad deals both buttons, that `NEXT` is on no frame with a
|
||||||
|
cursor accepting input, that the longest `NEXT`/`BACK` alternation is under four, and that every
|
||||||
|
battle it enters *ends* -- 25 of 25, worst 275 macros -- where v0.4.2 spent a whole hunt inside
|
||||||
|
one that never did. The battle rules are asked of the **scene the pad was dealt for** and not of
|
||||||
|
`wIsInBattle`: the `$ff` frame a lost battle passes through reads `Unknown`, whose pad is
|
||||||
|
`NEXT, BACK` by contract (row 9), and it is not under a battle's rules.
|
||||||
|
|
||||||
|
The decoder, the reward catalog, the adapter version and the compatibility string are untouched.
|
||||||
|
|
||||||
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
||||||
## priority to visit the shop at least once per area; make shop macros item purchases. same
|
## priority to visit the shop at least once per area; make shop macros item purchases. same
|
||||||
## for the Pokécenter. heal should be a macro.")
|
## for the Pokécenter. heal should be a macro.")
|
||||||
|
|
@ -936,12 +991,12 @@ observe is not a precondition, it is a guess.
|
||||||
| Menu (the start menu) | CLOSE, CONFIRM, BACK | unchanged. The cursor is the fly's to move with the **raw** D-pad, which still reaches the cartridge in macros mode; a SAVE or a POKéDEX button would be a macro per start-menu entry and is not asked for |
|
| Menu (the start menu) | CLOSE, CONFIRM, BACK | unchanged. The cursor is the fly's to move with the **raw** D-pad, which still reaches the cartridge in macros mode; a SAVE or a POKéDEX button would be a macro per start-menu entry and is not asked for |
|
||||||
| Menu (the bag, an elevator, the party list outside a battle) | CLOSE, CONFIRM, BACK | unchanged |
|
| Menu (the bag, an elevator, the party list outside a battle) | CLOSE, CONFIRM, BACK | unchanged |
|
||||||
| Unknown (the Pokédex, the trainer card, OPTION, a naming screen, a mid-warp frame) | NEXT, **BACK** | **BACK added** (row 9): B is what leaves the first three, and A leaves none of them |
|
| Unknown (the Pokédex, the trainer card, OPTION, a naming screen, a mid-warp frame) | NEXT, **BACK** | **BACK added** (row 9): B is what leaves the first three, and A leaves none of them |
|
||||||
| Battle, own turn, main menu | MOVE 1..4, SWITCH, ITEM, THROW BALL, RUN, NEXT | four move buttons for `ATTACK` (section 14); THROW BALL added, and gated on the species since 12.9; **RUN gated**, below. No `BACK`: the four entries are the answers to this menu |
|
| Battle, own turn, main menu | MOVE 1..4, SWITCH, ITEM, THROW BALL, RUN | four move buttons for `ATTACK` (section 14); THROW BALL added, and gated on the species since 12.9; **RUN gated**, below. No `BACK`: the four entries are the answers to this menu. **`NEXT` removed by 12.10** — an A press here confirms FIGHT and reopens the list the move list's `BACK` just closed, and `MOVE 1` is the backstop instead, bound here whatever the battler reads as |
|
||||||
| Battle, own turn, move list | MOVE 1..4, BACK | as above |
|
| Battle, own turn, move list | MOVE 1..4, BACK | as above |
|
||||||
| Battle, own turn, party list | SWITCH, BACK | unchanged |
|
| Battle, own turn, party list | SWITCH, BACK | unchanged |
|
||||||
| Battle, the bag | NEXT, **BACK** | **BACK added**, and the list now reports a *cursor* (`macros-wram.md` 7.1) — before this `ITEM` could open the bag and had nothing to read |
|
| Battle, own turn, the bag | ITEM, THROW BALL, **BACK** | the bag reports a *cursor* (`macros-wram.md` 7.1), and since **12.10** it is the own turn, because a cursor accepting input is one. Its pad is the list's own answers; `NEXT` and `CONFIRM` are both off it, being the same blind A press that *uses* whatever the cursor holds |
|
||||||
| Battle, forced switch | SWITCH, NEXT | unchanged (row 8) |
|
| Battle, forced switch | SWITCH, NEXT | unchanged (row 8). The one arm that keeps `NEXT` with a cursor up, because it cannot be cancelled and has no `BACK` to undo it |
|
||||||
| Battle, between turns | NEXT | `BACK` was added here for the bag (the row above reads as nobody’s turn) and **taken back out by section 12.9**: on a frame of battle text there is no list to leave, and a `BACK` that changes nothing is the trap of section 12.2 |
|
| Battle, between turns | NEXT | `BACK` was added here for the bag and **taken back out by 12.9**: on a frame of battle text there is no list to leave, and a `BACK` that changes nothing is the trap of section 12.2. Since **12.10** the bag is not on this row at all, so `NEXT` here is only ever the A that advances text |
|
||||||
| Shop | BUY POTION, BUY BALL, BUY ANTIDOTE, BUY REPEL, CONFIRM, LEAVE | two purchases to four; CONFIRM added |
|
| Shop | BUY POTION, BUY BALL, BUY ANTIDOTE, BUY REPEL, CONFIRM, LEAVE | two purchases to four; CONFIRM added |
|
||||||
| PC | **CONFIRM**, LEAVE | **CONFIRM added**: a list the fly opened is one it can answer rather than only close. Depositing and withdrawing are still not in the vocabulary (row 17) |
|
| PC | **CONFIRM**, LEAVE | **CONFIRM added**: a list the fly opened is one it can answer rather than only close. Depositing and withdrawing are still not in the vocabulary (row 17) |
|
||||||
| Title | nothing | unchanged, by contract: the readout's boot variant applies |
|
| Title | nothing | unchanged, by contract: the readout's boot variant applies |
|
||||||
|
|
@ -983,6 +1038,7 @@ measured where it cannot.
|
||||||
| a restore, before the first `observe` | not a cause: the loop calls `observe` once at the end of boot and once after a ratchet recovery, so the first frame is decided on a real palette |
|
| a restore, before the first `observe` | not a cause: the loop calls `observe` once at the end of boot and once after a ratchet recovery, so the first frame is decided on a real palette |
|
||||||
| the title screen, and raw mode | not an empty pad by contract: the readout's boot variant applies and no palette is dealt |
|
| the title screen, and raw mode | not an empty pad by contract: the readout's boot variant applies and no palette is dealt |
|
||||||
| a scene the detector cannot name | `Unknown` deals `NEXT` and `BACK`; a screen neither press leaves (the naming screen, row 14) is a genuine stall and still needs START, which is a contract change |
|
| a scene the detector cannot name | `Unknown` deals `NEXT` and `BACK`; a screen neither press leaves (the naming screen, row 14) is a genuine stall and still needs START, which is a contract change |
|
||||||
|
| the fly's own turn where the seam cannot read the battler, now that `NEXT` is off that row (12.10) | **fixed**: `MOVE 1` is bound over the top-level menu whatever `wBattleMon*` reads as, because FIGHT is one of that menu's four entries and always opens |
|
||||||
|
|
||||||
And because "closed where a macro can close it" is not "closed":
|
And because "closed where a macro can close it" is not "closed":
|
||||||
`game.padEmptyMs` publishes how long a **playable** scene has had nothing on the pad, in brain
|
`game.padEmptyMs` publishes how long a **playable** scene has had nothing on the pad, in brain
|
||||||
|
|
|
||||||
|
|
@ -621,3 +621,9 @@ then purge; then the stale-doc pass.
|
||||||
the window reader is the fallback. Verified against real presses on two maps. Trap hunt: 286 ->
|
the window reader is the fallback. Verified against real presses on two maps. Trap hunt: 286 ->
|
||||||
489 tiles, timeouts 6 -> 1; flagged windows rose 61 -> 70, all battle windows (the battle pad
|
489 tiles, timeouts 6 -> 1; flagged windows rose 61 -> 70, all battle windows (the battle pad
|
||||||
review in flight).
|
review in flight).
|
||||||
|
- 2026-09-22 (v0.4.3, loop review, auto): the watchdog flagged NEXT/BACK repeating 373 times in
|
||||||
|
ten brain minutes after v0.4.2. NEXT on the main battle menu confirmed FIGHT and opened the move
|
||||||
|
list, whose BACK closed it: a pair that undoes itself. NEXT is now off every pad with an
|
||||||
|
input-accepting cursor, MOVE 1 is the main menu backstop, the bag is the fly's turn. ROM test
|
||||||
|
fails on v0.4.2 and passes here. Trap hunt: 1 -> 260 tiles, windows under four tiles 73 -> 2.
|
||||||
|
Ships with map-aware walks. Ethos check held.
|
||||||
|
|
|
||||||
|
|
@ -1427,3 +1427,165 @@ tile counts can is that the fly is walking across maps instead of round the tile
|
||||||
`cargo test --workspace`, `cargo clippy --all-targets` and `infra/tests/lint.sh` on the development
|
`cargo test --workspace`, `cargo clippy --all-targets` and `infra/tests/lint.sh` on the development
|
||||||
box. `flysim --print-compatibility` is byte-identical across the change: 648 bytes,
|
box. `flysim --print-compatibility` is byte-identical across the change: 648 bytes,
|
||||||
`0d9bfde7…707fa`, so the live checkpoint carries over.
|
`0d9bfde7…707fa`, so the live checkpoint carries over.
|
||||||
|
## 2026-09-22, rows 42 and 43: the pair that undid itself
|
||||||
|
|
||||||
|
Thirty-five minutes after v0.4.2 deployed, the release watchdog flagged the same rung. Rank 9
|
||||||
|
(VIRIDIAN FOREST, next PEWTER CITY), seventy-one hours on it, and since the restart the macro
|
||||||
|
starts were `NEXT` **1264**, `BACK` **1241**, `THROW BALL` 5 and `GO WARP` 3, with the event log
|
||||||
|
alternating
|
||||||
|
|
||||||
|
```
|
||||||
|
NEXT start, NEXT done, BACK start, BACK done
|
||||||
|
```
|
||||||
|
|
||||||
|
every hold on map 51. It is the residual the previous review named and left -- "a `NEXT, BACK`
|
||||||
|
2-cycle x75 closes the run inside one battle... worth watching" -- at full scale, and the reason
|
||||||
|
that review's rule did not catch it is that **each of the two buttons was legitimate where it
|
||||||
|
stood**. `docs/design/macros.md` section 12.10 is the contract this closed against.
|
||||||
|
|
||||||
|
### The mechanism, measured
|
||||||
|
|
||||||
|
Twenty brain minutes from the release container's own rung-9 checkpoint, real cartridge, real
|
||||||
|
brain as the readout. The whole run -- **71,673 frames, all of them `battle`** -- was one battle,
|
||||||
|
on one tile, with 73 of 73 windows flagged and every window reading `NEXT, BACK` x74 or x75. The
|
||||||
|
two rows of the hunt's own sub-state table are the mechanism:
|
||||||
|
|
||||||
|
| macro start | battle sub-state | n |
|
||||||
|
| --- | --- | ---: |
|
||||||
|
| `BACK` | move list | **739** |
|
||||||
|
| `NEXT` | **main menu** | **739** |
|
||||||
|
| `NEXT` | move list, no cursor | 8 |
|
||||||
|
| `BACK` | party list | 1 |
|
||||||
|
| `MOVE 1` | main menu | 1 |
|
||||||
|
| `THROW BALL` | main menu | 1 |
|
||||||
|
|
||||||
|
1. **`NEXT` on the top-level battle menu is not the press that advances text.** `NEXT`'s script is
|
||||||
|
one press of A. Between turns that A advances the text box, which is the button's whole
|
||||||
|
purpose. On the top-level menu the same A press **confirms whatever the cursor sits on**, and
|
||||||
|
the cursor sits on FIGHT -- so `NEXT` *opened the move list*. It was put there as row 7's
|
||||||
|
backstop for a turn where every other button drops out, and it is the wrong button for that
|
||||||
|
job twice over: it does not end a turn, and what it does instead is reopen a list.
|
||||||
|
2. **`BACK` on the move list closed it again**, which is 12.9's contract and correct: backing out
|
||||||
|
of a list is one of exactly two answers to one. So neither rule was broken and the loop was a
|
||||||
|
*pair*: main menu -> `NEXT` -> move list -> `BACK` -> main menu, 739 times each, at 2.4 macros
|
||||||
|
per brain second, with the turn never resolving and the battle never ending. Section 12.2's
|
||||||
|
rule restated at the pad: **no pair of buttons on any battle pad may undo each other with
|
||||||
|
nothing else changing.**
|
||||||
|
3. **The bag was the last frame in the game with a cursor accepting input and no own turn.**
|
||||||
|
`Battle::own_turn` answered `false` for it (12.6 had no observable for the bag at all; 13.1 gave
|
||||||
|
it a cursor), so it landed on the between-turns row -- whose `NEXT` on an open bag is the A
|
||||||
|
press that *uses* whatever the cursor holds. Not what the live loop was made of, and the same
|
||||||
|
defect: an A press dealt where A does not advance text.
|
||||||
|
|
||||||
|
### What changed, all of it inside the macros
|
||||||
|
|
||||||
|
- **`NEXT` is off every own-turn pad**: the top-level menu, the move list, the party list and the
|
||||||
|
bag. It stays alone on the between-turns row and stays as row 8's backstop on the forced switch,
|
||||||
|
which is the one arm with a cursor up and no `BACK` at all, because it cannot be cancelled.
|
||||||
|
- **`MOVE 1` is the top-level menu's backstop instead**, and it is bound there whatever the seam
|
||||||
|
makes of `wBattleMon*`: the question over that menu is 12.8's "is there a move list to open",
|
||||||
|
FIGHT always opens, and `MOVE 1`'s script over that menu is "confirm FIGHT and stop", which reads
|
||||||
|
no move at all. A battler the seam cannot read is not a reason to take the turn's one ending
|
||||||
|
button away. Over an **open list** the per-slot PP rule is unchanged (row 30a, row 34).
|
||||||
|
- **The bag is the fly's turn** (`state::battle`), and its pad is the bag's own three answers:
|
||||||
|
`ITEM` (use what the cursor is on), `THROW BALL`, `BACK`. `CONFIRM` is gone from it -- the same
|
||||||
|
blind A press under another name -- and both scripts already navigated this list by reading its
|
||||||
|
cursor (`macros-wram.md` 7.1). The invariant is now whole: **a battle frame with a cursor
|
||||||
|
accepting input is the fly's turn**, the forced switch excepted because it has a pad of its own.
|
||||||
|
- **`wListMenuID` was checked for staleness rather than assumed sound**: it is zeroed by
|
||||||
|
`DisplayTextIDInit` at the start of every text display (`macros-wram.md`), so a battle's text
|
||||||
|
frames cannot inherit an `ITEMLISTMENU` from a bag the fly closed, and the bag reading stands.
|
||||||
|
- **The harness asks the scene the pad was dealt for, not `wIsInBattle`.** The `$ff` frame a lost
|
||||||
|
battle passes through reads `Unknown`, whose pad is `NEXT, BACK` by contract (row 9); the first
|
||||||
|
version of the new assertion accused that row of a battle rule it is not under.
|
||||||
|
|
||||||
|
| # | trap | trigger | test | fix, or why it is left |
|
||||||
|
| ---: | --- | --- | --- | --- |
|
||||||
|
| 42 | two buttons on a battle pad undo each other with nothing else changing: `NEXT` on the top-level menu opens the move list, `BACK` on the move list closes it | every wild battle, every turn -- 739 starts each in twenty brain minutes, one battle, 73 of 73 windows flagged | `no_battle_pad_holds_both_next_and_back`, `the_battle_row_is_the_move_buttons_switch_item_and_never_next`, `each_battle_menu_deals_its_own_pad`, `the_battles_turns_advance_from_the_rung_nine_forest_checkpoint` (ROM-gated: it **fails on v0.4.2** with `NEXT` dealt on `battle/main`) | **fixed**: `NEXT` is off every pad with a cursor accepting input, and `MOVE 1` is the top-level menu's backstop, bound there whatever the battler reads as. `NEXT` keeps the between-turns row and the forced switch |
|
||||||
|
| 43 | the battle bag reads as nobody's turn, so its pad is the between-turns `NEXT` -- an A press that *uses* what the cursor holds | choosing ITEM in a battle; rare, because `ITEM` needs a potion and a hurt Pokemon | `a_battle_frame_with_a_cursor_accepting_input_is_the_flys_turn`, `the_battle_bag_is_the_flys_turn_and_deals_its_own_two_uses`, `back_is_on_a_battle_pad_only_where_a_list_is_open` | **fixed**: `own_turn` is true for the bag, and its pad is `ITEM` / `THROW BALL` / `BACK`. This is row 30c closed -- it was left in 12.6 for want of an observable, given a cursor in 13.1, and given the right *turn* here |
|
||||||
|
|
||||||
|
### The ROM-gated run, before and after
|
||||||
|
|
||||||
|
Same test, same checkpoint, sixty-seven brain minutes of the game-blind rotation, `v0.4.2`
|
||||||
|
(`592c264`) against this branch.
|
||||||
|
|
||||||
|
| measure | before (v0.4.2) | after |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| verdict | **FAILED**: "`NEXT` was on the pad while a battle menu was accepting input" | **passed** |
|
||||||
|
| `NEXT` dealt on | `battle/between-turns`, **`battle/main`**, `battle/moves-unplaceable` | `battle/between-turns`, `battle/moves-unplaceable` |
|
||||||
|
| `BACK` dealt on | `battle/moves`, `battle/party` | `battle/bag`, `battle/moves`, `battle/party` |
|
||||||
|
| maps visited | **[51]** -- never left the forest | 51, 0, 12, 1, 41, 44, 42, 13, 50 (nine) |
|
||||||
|
| battles entered / ended | 14 / 15 | 26 / 25 |
|
||||||
|
| worst battle, in macros | not measurable: the run never left the fly's own turn | **275** |
|
||||||
|
| longest `NEXT`/`BACK` alternation | (the assertion fires first) | **1** |
|
||||||
|
| `MOVE 1..4` starts | 96 | **718** |
|
||||||
|
| `NEXT` / `BACK` starts | 1235 / 1315 of 3944 | 526 / 313 of 2090 |
|
||||||
|
| own-turn frames | 20,914 | 143,180 |
|
||||||
|
| `GO OBJECTIVE` on an overworld pad | map 51 only | nine maps |
|
||||||
|
|
||||||
|
### The trap hunt, before and after
|
||||||
|
|
||||||
|
Twenty brain minutes, seed 20260917, 4 sweep threads, the same connectome and the same cartridge,
|
||||||
|
from the release container's rung-9 checkpoint, **driven by the brain** rather than by
|
||||||
|
`FLY_TRAP_STUB`: this branch adds no macro type, so the thirty-one `macro_<type>` populations are
|
||||||
|
the same function in both arms and `--print-compatibility` is byte-identical at 648 bytes. The two
|
||||||
|
arms differ in the macro code and in nothing else.
|
||||||
|
|
||||||
|
| measure | before (v0.4.2) | after |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| distinct (map, tile) | **1** | **260** |
|
||||||
|
| windows flagged | 73/73 | **68/73** |
|
||||||
|
| windows under 4 tiles | **73** | **2** |
|
||||||
|
| longest repeated sequence in a window | **`NEXT, BACK` x75** | `NEXT` x20 |
|
||||||
|
| macros started | 1489 | 797 |
|
||||||
|
| frames in `battle` | **71,673** (one battle, the whole run) | 50,600 |
|
||||||
|
| frames in `overworld` | **0** | **20,394** |
|
||||||
|
| `NEXT` starts on the main menu | **739** | **0** |
|
||||||
|
| `BACK` starts on the move list | 739 | 263 |
|
||||||
|
| `MOVE n` starts | **1** | **113** |
|
||||||
|
| `THROW BALL` starts | 1 | 63 |
|
||||||
|
| `RUN` starts | 0 | 7 |
|
||||||
|
|
||||||
|
The before arm never left the battle it resumed in and never left the tile it stood on. The after
|
||||||
|
arm fought fifty thousand frames of battle *and* walked twenty thousand frames of overworld across
|
||||||
|
260 tiles.
|
||||||
|
|
||||||
|
Raw reports: `hunt-before-20260922T0459.md` and `hunt-after-20260922T0459.md` in the coordination
|
||||||
|
state's `runs/` directory.
|
||||||
|
|
||||||
|
### Residuals, named rather than worked around
|
||||||
|
|
||||||
|
- **`NEXT` on a move list whose cursor the seam cannot place is now the largest source of it** --
|
||||||
|
142 of the after arm's 248 `NEXT` starts, over 8,687 frames. That frame is *correctly* not the
|
||||||
|
fly's turn (row 30b: `MoveSelectionMenu`'s coordinates appear before the engine has copied the
|
||||||
|
active Pokemon into `wBattleMon*`), so `NEXT` there is the between-turns press and it advances.
|
||||||
|
It is not a 2-cycle -- the ROM-gated run measures the longest `NEXT`/`BACK` alternation at
|
||||||
|
**1** -- but it is the same unplaceable cursor 12.6 named, and the honest fix is still a WRAM
|
||||||
|
reading rather than a pad change.
|
||||||
|
- **`BACK` is 326 of 797 starts**, all of them over an open list: 263 on the move list and 63 on a
|
||||||
|
party list. Row 34's contract, and where it leads is a menu with `MOVE 1` on it.
|
||||||
|
- **The 68 windows still flagged are the sequence rule's dominance arm, not a cycle.** They read
|
||||||
|
`NEXT` x11 to x20 or `BACK` x11 to x14 over windows holding **5 to 86** distinct tiles, where
|
||||||
|
every window of the before arm read `NEXT, BACK` x74 on **one** tile. Only two windows of the
|
||||||
|
after arm are under four tiles, against seventy-three before. A window spent in a long battle is
|
||||||
|
one tile by construction, which is the known false-positive shape of the tile rule and why this
|
||||||
|
row is proved on the pad and sub-state tables as well.
|
||||||
|
- **Row 41 is not reproduced here and is not closed.** The after arm's text boxes are 140 frames
|
||||||
|
at (6, 30) of map 0x33 and smaller counts on seven other tiles, with no Pokemon Center in the
|
||||||
|
run at all; the nurse's `YES` loop needs its own checkpoint to measure.
|
||||||
|
- **The road north is still reported rather than asserted.** The ROM-gated run leaves the forest
|
||||||
|
and reaches maps 50, 13, 12, 1, 41, 44, 42 and 0 with `GO OBJECTIVE` on the overworld pad of all
|
||||||
|
nine, but it does not reach map 47 -- the forest's north gate -- in sixty-seven brain minutes of
|
||||||
|
the game-blind rotation. Row 40's note stands.
|
||||||
|
|
||||||
|
### Gates
|
||||||
|
|
||||||
|
- `cargo test --workspace` with `FLY_ROM` set: green except
|
||||||
|
`flysim::integration::the_service_streams_takes_sugar_checkpoints_and_resumes_after_being_killed`,
|
||||||
|
which fails identically on v0.4.2 on this box with the same assertion (a debug build of the
|
||||||
|
service does not finish booting inside the test's window here). Pre-existing and unrelated to
|
||||||
|
the macro layer.
|
||||||
|
- `cargo clippy --all-targets`: clean.
|
||||||
|
- `infra/tests/lint.sh`: all checks passed, de-PII guard included.
|
||||||
|
- `--print-compatibility`: **648 bytes, sha256 `0d9bfde7...707fa`** -- byte-identical to v0.4.1 and
|
||||||
|
v0.4.2. Decoder, reward catalog, adapter version and roles untouched.
|
||||||
|
|
|
||||||
|
|
@ -504,35 +504,44 @@ pub fn scene_set(scene: Scene, state: &mut dyn MacroState) -> Vec<MacroKind> {
|
||||||
// `SWITCH` needs a Pokémon to switch *to* and a forced switch with none leaves a menu the
|
// `SWITCH` needs a Pokémon to switch *to* and a forced switch with none leaves a menu the
|
||||||
// game will not let the fly cancel and a pad with nothing on it (row 8).
|
// game will not let the fly cancel and a pad with nothing on it (row 8).
|
||||||
Scene::Battle { forced_switch: true, .. } => vec![Switch, Next],
|
Scene::Battle { forced_switch: true, .. } => vec![Switch, Next],
|
||||||
// The fly's turn, by which menu of it is accepting input (section 12.6). `NEXT` is on none
|
// The fly's turn, by which menu of it is accepting input (section 12.6). **`NEXT` is on
|
||||||
// of the two lists: an A press on whatever the cursor happens to be sitting on is what the
|
// no own-turn pad at all** (section 12.10): it is the A that advances *text*, and on a menu
|
||||||
// live loop was made of.
|
// that is accepting input the same A press opens or confirms whatever the cursor happens to
|
||||||
|
// be sitting on, which is never one of the answers to that menu.
|
||||||
|
//
|
||||||
|
// Live on rung 9 after v0.4.2, 71 hours in Viridian Forest: `NEXT` 1264 macro starts and
|
||||||
|
// `BACK` 1241, the log alternating `NEXT start/done, BACK start/done` every hold. `NEXT` on
|
||||||
|
// the *top-level* menu pressed A on FIGHT and opened the move list; `BACK` on the *move
|
||||||
|
// list* closed it again; neither spent a turn, and the two of them were half the pad
|
||||||
|
// between them. Two buttons that undo each other with nothing else changing are section
|
||||||
|
// 12.2's trap spread over two sub-states of one turn.
|
||||||
Scene::Battle { own_turn: true, .. } => match battle_menu(state) {
|
Scene::Battle { own_turn: true, .. } => match battle_menu(state) {
|
||||||
BattleMenu::Moves { .. } => vec![Move1, Move2, Move3, Move4, Back],
|
BattleMenu::Moves { .. } => vec![Move1, Move2, Move3, Move4, Back],
|
||||||
BattleMenu::Party { .. } => vec![Switch, Back],
|
BattleMenu::Party { .. } => vec![Switch, Back],
|
||||||
// The bag reads as nobody's turn (`state::battle`), so it cannot land here; the arm
|
// The bag, which is the fly's turn since 12.10. Its three answers: use the thing the
|
||||||
// exists because `own_turn` is a boolean and this match is over the menu.
|
// cursor is on (`ITEM`), throw the ball (`THROW BALL`), or leave the list (`BACK`).
|
||||||
BattleMenu::Bag { .. } => vec![Confirm, Back],
|
// `CONFIRM` was an A press on whatever the cursor held, which is the same press
|
||||||
|
// `NEXT` was and reads nothing; the two scripts navigate the list's own cursor.
|
||||||
|
BattleMenu::Bag { .. } => vec![Item, ThrowBall, Back],
|
||||||
|
// `None` cannot land here -- `own_turn` is false for it -- and the arm exists because
|
||||||
|
// `own_turn` is a boolean and this match is over the menu. `MOVE 1` is what keeps this
|
||||||
|
// pad from ever being one the fly cannot end the turn from: FIGHT is one of these four
|
||||||
|
// entries and it always opens (section 12.8), which is the backstop `NEXT` was.
|
||||||
BattleMenu::Main { .. } | BattleMenu::None => {
|
BattleMenu::Main { .. } | BattleMenu::None => {
|
||||||
vec![Move1, Move2, Move3, Move4, Switch, Item, ThrowBall, Run, Next]
|
vec![Move1, Move2, Move3, Move4, Switch, Item, ThrowBall, Run]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 2026-09-16 hotfix (live deadlock on Route 1): battle text between turns waits for a
|
// 2026-09-16 hotfix (live deadlock on Route 1): battle text between turns waits for a
|
||||||
// press exactly like a dialog. Section 13.1 added `BACK` here for the bag a battle's ITEM
|
// press exactly like a dialog.
|
||||||
// entry opens, which is none of `BattleMenu`'s three and reads as nobody's turn -- and
|
|
||||||
// section 12.9 narrows it back to that list.
|
|
||||||
//
|
//
|
||||||
// **`BACK` is a button only where there is a list to leave.** Live on rung 9, 69 hours in
|
// **One button, and it is the A that advances text.** Section 13.1 added `BACK` here for
|
||||||
// Viridian Forest: `BACK` was 135 of 183 macro starts since the restart, `RUN blocked,
|
// the bag, because a bag read as nobody's turn; 12.9 narrowed it back to the bag alone;
|
||||||
// BACK start, BACK done` over and over. Between turns there is nothing open to back out
|
// 12.10 moves the bag to the own turn where its cursor says it belongs, so nothing is open
|
||||||
// of, so the B press changes nothing the `NEXT` beside it does not, the macro completes on
|
// on this row any more and nothing but `NEXT` is on it. A frame that reaches here has no
|
||||||
// the tile it started on in a handful of frames, and the roll lands on it most holds while
|
// cursor accepting input -- text, an animation, a turn resolving -- so there is nothing to
|
||||||
// the turn does not move. That is section 12.2's trap exactly: a macro that completes
|
// back out of (`BACK` would be 12.2's trap, the rung-9 loop of 12.9) and nothing for an A
|
||||||
// without moving because its precondition is already satisfied where the fly stands.
|
// press to open (`NEXT` here cannot be the press that opened a menu, which is 12.10).
|
||||||
Scene::Battle { .. } => match battle_menu(state) {
|
Scene::Battle { .. } => vec![Next],
|
||||||
BattleMenu::Bag { .. } => vec![Next, Back],
|
|
||||||
_ => vec![Next],
|
|
||||||
},
|
|
||||||
// Section 13: the shop's buttons are the four purchases, plus the two answers any list has.
|
// Section 13: the shop's buttons are the four purchases, plus the two answers any list has.
|
||||||
Scene::Shop => vec![BuyPotion, BuyBall, BuyAntidote, BuyRepel, Confirm, Leave],
|
Scene::Shop => vec![BuyPotion, BuyBall, BuyAntidote, BuyRepel, Confirm, Leave],
|
||||||
// Section 13.1: `CONFIRM` as well as `LEAVE`, so a PC the fly opened is a list it can
|
// Section 13.1: `CONFIRM` as well as `LEAVE`, so a PC the fly opened is a list it can
|
||||||
|
|
@ -1579,7 +1588,18 @@ pub const fn move_index(kind: MacroKind) -> Option<u8> {
|
||||||
/// one at all -- which nothing in the game reaches -- is the one case that still answers `false`.
|
/// one at all -- which nothing in the game reaches -- is the one case that still answers `false`.
|
||||||
pub fn move_slot_bound(state: &mut dyn MacroState, kind: MacroKind) -> bool {
|
pub fn move_slot_bound(state: &mut dyn MacroState, kind: MacroKind) -> bool {
|
||||||
let Some(index) = move_index(kind) else { return false };
|
let Some(index) = move_index(kind) else { return false };
|
||||||
let Some(own) = state.battle().and_then(|battle| battle.own) else { return false };
|
let Some(battle) = state.battle() else { return false };
|
||||||
|
// Over the **top-level menu** the question is section 12.8's -- "is there a move list to
|
||||||
|
// open" -- and FIGHT always opens: what is behind it is the cartridge's business, because
|
||||||
|
// `CheckPlayerHasUsableMoves` prints "has no moves left!" and sets Struggle without opening
|
||||||
|
// the list. `MOVE 1`'s script over that menu is "confirm FIGHT and stop" and reads no move at
|
||||||
|
// all, so the button is bound there whatever the seam can make of the battler. That is the
|
||||||
|
// backstop `NEXT` used to be on this row (section 12.10): the own turn's main menu always has
|
||||||
|
// a button that ends the turn, and it is never one that merely reopens a list.
|
||||||
|
if index == 0 && matches!(battle.menu, BattleMenu::Main { .. }) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(own) = battle.own else { return false };
|
||||||
let holds = |slot: usize| -> Option<&Move> {
|
let holds = |slot: usize| -> Option<&Move> {
|
||||||
own.moves.get(slot).and_then(|entry| entry.as_ref()).filter(|entry| entry.id != 0)
|
own.moves.get(slot).and_then(|entry| entry.as_ref()).filter(|entry| entry.id != 0)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ pub enum Scene {
|
||||||
Dialog,
|
Dialog,
|
||||||
/// Start menu or one of its submenus, outside battle.
|
/// Start menu or one of its submenus, outside battle.
|
||||||
Menu,
|
Menu,
|
||||||
/// In a battle. `own_turn` is the top-level FIGHT/PKMN/ITEM/RUN menu waiting for input;
|
/// In a battle. `own_turn` is any battle menu waiting for input -- the top-level
|
||||||
|
/// FIGHT/PKMN/ITEM/RUN one, the move list, the party list or the bag;
|
||||||
/// `forced_switch` is the party list the game opens when the active Pokémon has fainted, which
|
/// `forced_switch` is the party list the game opens when the active Pokémon has fainted, which
|
||||||
/// cannot be backed out of.
|
/// cannot be backed out of.
|
||||||
Battle { own_turn: bool, forced_switch: bool },
|
Battle { own_turn: bool, forced_switch: bool },
|
||||||
|
|
@ -234,10 +235,10 @@ pub enum BattleMenu {
|
||||||
/// The bag, opened from a battle's ITEM entry: `wListMenuID` is `ITEMLISTMENU`.
|
/// The bag, opened from a battle's ITEM entry: `wListMenuID` is `ITEMLISTMENU`.
|
||||||
///
|
///
|
||||||
/// Not one of the three `docs/design/macros.md` section 12.6 named, and the gap was
|
/// Not one of the three `docs/design/macros.md` section 12.6 named, and the gap was
|
||||||
/// observable: the bag list is not the top-level menu, so `own_turn` is false and the frame
|
/// observable twice over. First the list needed a *cursor* the scripts can read, which is what
|
||||||
/// reads as a battle between turns, whose pad is `NEXT` and `BACK` -- and `NEXT` was an A
|
/// `ITEM` and section 14's `THROW BALL` navigate by (2026-09-17). Then it needed to be the
|
||||||
/// press on whatever the cursor held. What the list needed is a *cursor* the scripts can read,
|
/// fly's *turn*: a bag reading as nobody's turn landed on the between-turns row, whose `NEXT`
|
||||||
/// which is what `ITEM` and section 14's `THROW BALL` navigate by (2026-09-17).
|
/// is the A that advances text and on an open bag is the A that uses an item (12.10).
|
||||||
Bag { cursor: u8, count: u8 },
|
Bag { cursor: u8, count: u8 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,8 +246,9 @@ pub enum BattleMenu {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Battle {
|
pub struct Battle {
|
||||||
pub kind: BattleKind,
|
pub kind: BattleKind,
|
||||||
/// A battle menu is open and waiting for the fly: the top-level one, the move list, or the
|
/// A battle menu is open and waiting for the fly: the top-level one, the move list, the bag,
|
||||||
/// party list outside a forced switch (`pokemon_red::state::battle`).
|
/// or the party list outside a forced switch (`pokemon_red::state::battle`). Every frame with a
|
||||||
|
/// cursor accepting input is one of these, which is section 12.10's invariant.
|
||||||
pub own_turn: bool,
|
pub own_turn: bool,
|
||||||
/// The party list is open because the active Pokémon fainted; it cannot be cancelled.
|
/// The party list is open because the active Pokémon fainted; it cannot be cancelled.
|
||||||
pub forced_switch: bool,
|
pub forced_switch: bool,
|
||||||
|
|
|
||||||
|
|
@ -924,17 +924,22 @@ fn the_menu_row_is_close_confirm_back() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_battle_row_is_the_move_buttons_switch_item_and_the_backstop() {
|
fn the_battle_row_is_the_move_buttons_switch_item_and_never_next() {
|
||||||
let mut world = World::battle();
|
let mut world = World::battle();
|
||||||
world.bag = vec![(item::POTION, 1)];
|
world.bag = vec![(item::POTION, 1)];
|
||||||
let scene = world.scene();
|
let scene = world.scene();
|
||||||
// Section 13.1: `RUN` is bound only in a wild battle the fly is *losing*, and `World::battle`
|
// Section 13.1: `RUN` is bound only in a wild battle the fly is *losing*, and `World::battle`
|
||||||
// has an eighteen-of-twenty Pokémon on the bench -- something healthier to send in, so the
|
// has an eighteen-of-twenty Pokémon on the bench -- something healthier to send in, so the
|
||||||
// fight is still worth having and the slot is empty.
|
// fight is still worth having and the slot is empty.
|
||||||
|
//
|
||||||
|
// Section 12.10: **`NEXT` is gone from this row.** It was the backstop for a turn where every
|
||||||
|
// other button dropped, and it was an A press on the cursor -- which sits on FIGHT, so it
|
||||||
|
// opened the move list, whose `BACK` closed it again: `NEXT` 1264 starts and `BACK` 1241 on
|
||||||
|
// rung 9 after v0.4.2. `MOVE 1` is the backstop now, and it ends the turn.
|
||||||
let palette = Palette::for_scene(scene, &mut world);
|
let palette = Palette::for_scene(scene, &mut world);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
names(&palette),
|
names(&palette),
|
||||||
["NEXT", "MOVE 1", "MOVE 2", "MOVE 3", "SWITCH", "ITEM"],
|
["MOVE 1", "MOVE 2", "MOVE 3", "SWITCH", "ITEM"],
|
||||||
"three moves with PP, an empty fourth slot, a healthy bench and a potion"
|
"three moves with PP, an empty fourth slot, a healthy bench and a potion"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -943,15 +948,17 @@ fn the_battle_row_is_the_move_buttons_switch_item_and_the_backstop() {
|
||||||
let scene = world.scene();
|
let scene = world.scene();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
names(&Palette::for_scene(scene, &mut world)),
|
names(&Palette::for_scene(scene, &mut world)),
|
||||||
["NEXT", "MOVE 1", "MOVE 2", "MOVE 3", "ITEM", "RUN"]
|
["MOVE 1", "MOVE 2", "MOVE 3", "ITEM", "RUN"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_move_button_is_bound_by_its_own_slots_pp_and_move_one_carries_struggle() {
|
fn a_move_button_is_bound_by_its_own_slots_pp_and_move_one_carries_struggle() {
|
||||||
// Section 14: one button per move slot. `World::battle`'s Pokémon has three moves and an empty
|
// Section 14: one button per move slot, asked **over the open move list**, which is where a
|
||||||
// fourth slot, so three buttons are on the pad and the fourth never is.
|
// slot is a thing to aim at. `World::battle`'s Pokémon has three moves and an empty fourth
|
||||||
|
// slot, so three buttons are on the list's pad and the fourth never is.
|
||||||
let mut world = World::battle();
|
let mut world = World::battle();
|
||||||
|
world.list = List::Moves(3);
|
||||||
assert!(move_slot_bound(&mut world, MacroKind::Move1));
|
assert!(move_slot_bound(&mut world, MacroKind::Move1));
|
||||||
assert!(move_slot_bound(&mut world, MacroKind::Move2));
|
assert!(move_slot_bound(&mut world, MacroKind::Move2));
|
||||||
assert!(move_slot_bound(&mut world, MacroKind::Move3));
|
assert!(move_slot_bound(&mut world, MacroKind::Move3));
|
||||||
|
|
@ -975,12 +982,25 @@ fn a_move_button_is_bound_by_its_own_slots_pp_and_move_one_carries_struggle() {
|
||||||
.is_some()
|
.is_some()
|
||||||
);
|
);
|
||||||
|
|
||||||
// A Pokémon with no move in slot one at all is the one case that answers no. Nothing in the
|
// A Pokémon with no move in slot one at all answers no over the list: there is no slot to aim
|
||||||
// game reaches it, and inventing a press for it is what this crate does not do.
|
// at, and inventing a press for it is what this crate does not do.
|
||||||
world.mons[0] = mon(0, 4, 20, &[]);
|
world.mons[0] = mon(0, 4, 20, &[]);
|
||||||
assert!(!move_slot_bound(&mut world, MacroKind::Move1));
|
assert!(!move_slot_bound(&mut world, MacroKind::Move1));
|
||||||
let scene = world.scene();
|
let scene = world.scene();
|
||||||
assert_eq!(Palette::for_scene(scene, &mut world).slot(MacroId(MacroKind::Move1.slot())), None);
|
assert_eq!(Palette::for_scene(scene, &mut world).slot(MacroId(MacroKind::Move1.slot())), None);
|
||||||
|
|
||||||
|
// Over the **top-level menu** the question is a different one -- section 12.8's "is there a
|
||||||
|
// move list to open" -- and FIGHT always opens, so `MOVE 1` is bound there whatever the seam
|
||||||
|
// makes of the battler. That is what carries the turn now that `NEXT` is off this row
|
||||||
|
// (section 12.10): the script over this menu is "confirm FIGHT and stop" and reads no move.
|
||||||
|
world.list = List::BattleMain;
|
||||||
|
assert!(move_slot_bound(&mut world, MacroKind::Move1), "FIGHT is always pressable");
|
||||||
|
assert!(!move_slot_bound(&mut world, MacroKind::Move2), "and it is MOVE 1 that carries it");
|
||||||
|
world.active = None;
|
||||||
|
assert!(
|
||||||
|
move_slot_bound(&mut world, MacroKind::Move1),
|
||||||
|
"a battler the seam cannot read is not a reason to take the turn's one button away"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1097,16 +1117,21 @@ fn a_battle_frame_that_is_not_the_players_turn_binds_next_to_advance_its_text()
|
||||||
world.list = List::None;
|
world.list = List::None;
|
||||||
let scene = world.scene();
|
let scene = world.scene();
|
||||||
// Section 12.9: `NEXT` alone. Section 13.1 put `BACK` here for the bag a battle's ITEM entry
|
// Section 12.9: `NEXT` alone. Section 13.1 put `BACK` here for the bag a battle's ITEM entry
|
||||||
// opens, which reads as nobody's turn -- but on a frame of text there is no list to leave, and
|
// opens, which read as nobody's turn -- but on a frame of text there is no list to leave, and
|
||||||
// a `BACK` that changes nothing is the trap of section 12.2 (live, rung 9: 135 of 183 macro
|
// a `BACK` that changes nothing is the trap of section 12.2 (live, rung 9: 135 of 183 macro
|
||||||
// starts).
|
// starts).
|
||||||
let palette = Palette::for_scene(scene, &mut world);
|
let palette = Palette::for_scene(scene, &mut world);
|
||||||
assert_eq!(names(&palette), ["NEXT"]);
|
assert_eq!(names(&palette), ["NEXT"]);
|
||||||
|
|
||||||
// The bag is the one sub-state on this arm with a list open in it, and it keeps `BACK`.
|
// Section 12.10: the bag does not land on this arm any more. It is a cursor accepting input,
|
||||||
|
// so it is the fly's turn, and its `NEXT` would have been the A that *uses* what the cursor
|
||||||
|
// holds rather than the A that advances text. Nothing is open here, so nothing but `NEXT` is.
|
||||||
world.list = List::BattleBag;
|
world.list = List::BattleBag;
|
||||||
|
world.battle = Some((BattleKind::Wild, true, false));
|
||||||
|
world.scene = Scene::Battle { own_turn: true, forced_switch: false };
|
||||||
|
world.bag = vec![(item::POTION, 1)];
|
||||||
let palette = Palette::for_scene(world.scene(), &mut world);
|
let palette = Palette::for_scene(world.scene(), &mut world);
|
||||||
assert_eq!(names(&palette), ["NEXT", "BACK"]);
|
assert_eq!(names(&palette), ["BACK", "ITEM"], "a hurt Pokémon, a potion, and no ball");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -2356,10 +2381,11 @@ fn the_battle_plan_attacks_first_and_switches_only_under_a_quarter() {
|
||||||
let mut healthy = World::battle();
|
let mut healthy = World::battle();
|
||||||
healthy.mons[0] = mon(0, 20, 20, &[(33, 30)]);
|
healthy.mons[0] = mon(0, 20, 20, &[(33, 30)]);
|
||||||
assert!(!plan::failing(&mut healthy));
|
assert!(!plan::failing(&mut healthy));
|
||||||
// `NEXT` is unconditional: a battle frame always has a press that advances it, which is what
|
// `MOVE 1` is what keeps the own-turn pad from being empty when every other button drops out:
|
||||||
// keeps the own-turn pad from being empty when every other button drops out (2026-09-17).
|
// FIGHT is one of this menu's four entries and it always opens (12.8), where the `NEXT` that
|
||||||
// `ITEM` needs a potion and low HP, so it is absent here; this Pokémon has one move.
|
// used to carry the job merely reopened the list `BACK` had just closed (12.10). `ITEM` needs
|
||||||
assert_eq!(plan(&mut healthy), ["NEXT", "MOVE 1", "SWITCH"]);
|
// a potion and low HP, so it is absent here; this Pokémon has one move.
|
||||||
|
assert_eq!(plan(&mut healthy), ["MOVE 1", "SWITCH"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -2372,7 +2398,7 @@ fn the_battle_plan_runs_from_a_wild_battle_only_when_the_whole_party_is_weak() {
|
||||||
// the next hit ends the battle, which is the state section 9 puts `RUN` in.
|
// the next hit ends the battle, which is the state section 9 puts `RUN` in.
|
||||||
world.mons = vec![mon(0, 4, 20, &[(33, 30)]), mon(1, 3, 20, &[(33, 30)])];
|
world.mons = vec![mon(0, 4, 20, &[(33, 30)]), mon(1, 3, 20, &[(33, 30)])];
|
||||||
assert!(plan::party_weak(&mut world));
|
assert!(plan::party_weak(&mut world));
|
||||||
assert_eq!(plan(&mut world), ["NEXT", "MOVE 1", "SWITCH", "RUN"]);
|
assert_eq!(plan(&mut world), ["MOVE 1", "SWITCH", "RUN"]);
|
||||||
|
|
||||||
// A trainer battle has no RUN at all, in the plan or in the palette.
|
// A trainer battle has no RUN at all, in the plan or in the palette.
|
||||||
world.battle = Some((BattleKind::Trainer, true, false));
|
world.battle = Some((BattleKind::Trainer, true, false));
|
||||||
|
|
@ -3244,7 +3270,11 @@ fn each_battle_menu_deals_its_own_pad() {
|
||||||
let pad = names(&plan::plan_for(main.scene(), &mut main));
|
let pad = names(&plan::plan_for(main.scene(), &mut main));
|
||||||
assert!(pad.contains(&"MOVE 1"), "{pad:?}");
|
assert!(pad.contains(&"MOVE 1"), "{pad:?}");
|
||||||
assert!(pad.contains(&"SWITCH"), "{pad:?}");
|
assert!(pad.contains(&"SWITCH"), "{pad:?}");
|
||||||
assert!(pad.contains(&"NEXT"), "the top-level menu keeps its backstop: {pad:?}");
|
// Section 12.10: the top-level menu is a list accepting input too, so `NEXT` is off it as
|
||||||
|
// well. Its backstop is `MOVE 1`, which is bound here whatever the battler reads as, because
|
||||||
|
// FIGHT always opens (12.8) -- and unlike `NEXT` it ends the turn instead of opening the list
|
||||||
|
// that `BACK` closes again.
|
||||||
|
assert!(!pad.contains(&"NEXT"), "never NEXT on a menu accepting input: {pad:?}");
|
||||||
// `ITEM` and `RUN` have their own preconditions -- a potion, and a party with nothing healthy
|
// `ITEM` and `RUN` have their own preconditions -- a potion, and a party with nothing healthy
|
||||||
// left -- and this fixture satisfies neither; row 7 covers the turn where all four drop.
|
// left -- and this fixture satisfies neither; row 7 covers the turn where all four drop.
|
||||||
|
|
||||||
|
|
@ -3321,11 +3351,8 @@ fn back_is_on_a_battle_pad_only_where_a_list_is_open() {
|
||||||
let mut party = World::battle();
|
let mut party = World::battle();
|
||||||
party.list = List::BattleParty;
|
party.list = List::BattleParty;
|
||||||
with_back(&mut party);
|
with_back(&mut party);
|
||||||
// The bag reads as nobody's turn (`state::battle`), so it lands on the between-turns arm --
|
// The bag, which is the fly's own turn since section 12.10 because its cursor accepts input.
|
||||||
// and it is the one sub-state there with a list open in it.
|
|
||||||
let mut bag = World::battle();
|
let mut bag = World::battle();
|
||||||
bag.scene = Scene::Battle { own_turn: false, forced_switch: false };
|
|
||||||
bag.battle = Some((BattleKind::Wild, false, false));
|
|
||||||
bag.list = List::BattleBag;
|
bag.list = List::BattleBag;
|
||||||
with_back(&mut bag);
|
with_back(&mut bag);
|
||||||
|
|
||||||
|
|
@ -3344,6 +3371,93 @@ fn back_is_on_a_battle_pad_only_where_a_list_is_open() {
|
||||||
without(&mut forced);
|
without(&mut forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Section 12.10: **no battle pad deals a pair of buttons that undo each other.**
|
||||||
|
///
|
||||||
|
/// Live on rung 9 after v0.4.2, 71 hours in Viridian Forest: `NEXT` 1264 macro starts, `BACK`
|
||||||
|
/// 1241, and the event log alternating `NEXT start/done, BACK start/done` every hold on map 51.
|
||||||
|
/// The pair was split across two sub-states of one turn -- `NEXT` on the top-level menu was an A
|
||||||
|
/// press on FIGHT, which opened the move list, and `BACK` on the move list closed it again -- so
|
||||||
|
/// neither the pad rule of 12.9 nor the "no `BACK` without a list" rule caught it: both buttons
|
||||||
|
/// were legitimate where they stood, and between them they were a 2-cycle that never spent a turn.
|
||||||
|
///
|
||||||
|
/// The rule that closes it is about the pair rather than about either button: `NEXT` is the A that
|
||||||
|
/// advances **text**, so it belongs only on a frame with no cursor accepting input, and `BACK` is
|
||||||
|
/// the B that leaves a **list**, so it belongs only on a frame that has one. The two conditions are
|
||||||
|
/// exclusive, so no pad can hold both -- and the forced switch, which keeps `NEXT` as row 8's
|
||||||
|
/// backstop, is the one arm with a cursor and no `BACK` at all, because it cannot be cancelled.
|
||||||
|
#[test]
|
||||||
|
fn no_battle_pad_holds_both_next_and_back() {
|
||||||
|
// Every battle sub-state the seam can report, on a turn where every precondition is satisfied
|
||||||
|
// (a potion, a ball, a hurt Pokémon, a bench) and on one where none is.
|
||||||
|
let sub_states = [
|
||||||
|
(Scene::Battle { own_turn: true, forced_switch: false }, true, false, List::BattleMain),
|
||||||
|
(Scene::Battle { own_turn: true, forced_switch: false }, true, false, List::Moves(3)),
|
||||||
|
(Scene::Battle { own_turn: true, forced_switch: false }, true, false, List::BattleParty),
|
||||||
|
(Scene::Battle { own_turn: true, forced_switch: false }, true, false, List::BattleBag),
|
||||||
|
(Scene::Battle { own_turn: false, forced_switch: false }, false, false, List::None),
|
||||||
|
(Scene::Battle { own_turn: false, forced_switch: true }, false, true, List::BattleParty),
|
||||||
|
];
|
||||||
|
for stocked in [false, true] {
|
||||||
|
for (scene, own_turn, forced, list) in sub_states {
|
||||||
|
let mut world = World::battle();
|
||||||
|
world.scene = scene;
|
||||||
|
world.battle = Some((BattleKind::Wild, own_turn, forced));
|
||||||
|
world.list = list;
|
||||||
|
if stocked {
|
||||||
|
world.bag = vec![(item::POTION, 1), (item::POKE_BALL, 3)];
|
||||||
|
world.enemy = Some(EnemyMon { species: 0x99, level: 3, hp: 5, max_hp: 11 });
|
||||||
|
}
|
||||||
|
let pad = names(&plan::plan_for(world.scene(), &mut world));
|
||||||
|
let next = pad.contains(&"NEXT");
|
||||||
|
let back = pad.contains(&"BACK");
|
||||||
|
assert!(
|
||||||
|
!(next && back),
|
||||||
|
"{list:?} (stocked {stocked}) deals a pair that undoes itself: {pad:?}"
|
||||||
|
);
|
||||||
|
// And the pair is not the only way to waste a hold: a pad of one button that cannot
|
||||||
|
// end the turn is the shape row 34 had, so every sub-state is checked for having one.
|
||||||
|
assert!(
|
||||||
|
!pad.is_empty(),
|
||||||
|
"{list:?} (stocked {stocked}) deals nothing: {pad:?}"
|
||||||
|
);
|
||||||
|
// `NEXT` is on a frame with no cursor accepting input, or on the forced switch that
|
||||||
|
// has no other answer (row 8). Nowhere else.
|
||||||
|
if next {
|
||||||
|
assert!(
|
||||||
|
forced || list == List::None,
|
||||||
|
"NEXT on a menu accepting input: {list:?} deals {pad:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Section 12.10: the bag inside a battle is the fly's turn, and its pad is the bag's answers.
|
||||||
|
///
|
||||||
|
/// `NEXT` on an open bag is the A press that **uses** whatever the cursor is sitting on, which is
|
||||||
|
/// not one of the answers to a list, and `CONFIRM` beside `BACK` was the same press by another
|
||||||
|
/// name. The two scripts that navigate this list by reading its cursor are `ITEM` and
|
||||||
|
/// `THROW BALL`, and they are what the row deals.
|
||||||
|
#[test]
|
||||||
|
fn the_battle_bag_is_the_flys_turn_and_deals_its_own_two_uses() {
|
||||||
|
let mut world = World::battle();
|
||||||
|
world.list = List::BattleBag;
|
||||||
|
world.bag = vec![(item::POTION, 2), (item::POKE_BALL, 4)];
|
||||||
|
world.enemy = Some(EnemyMon { species: 0x99, level: 3, hp: 5, max_hp: 11 });
|
||||||
|
// `World::battle` is a wild battle with the active Pokémon on 4 of 20, so `ITEM`'s two facts
|
||||||
|
// hold and `THROW BALL`'s four do: a wild battle, a ball, room in a party of three, and an
|
||||||
|
// enemy species the party does not hold.
|
||||||
|
assert_eq!(
|
||||||
|
names(&plan::plan_for(world.scene(), &mut world)),
|
||||||
|
["BACK", "ITEM", "THROW BALL"]
|
||||||
|
);
|
||||||
|
// Nothing in the bag: leaving the list is the press, and there is no `NEXT` to use a thing
|
||||||
|
// that is not there.
|
||||||
|
world.bag.clear();
|
||||||
|
let pad = names(&plan::plan_for(world.scene(), &mut world));
|
||||||
|
assert_eq!(pad, ["BACK"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_spent_slot_is_off_the_pad_and_a_slot_with_pp_is_on_it_over_an_open_list() {
|
fn a_spent_slot_is_off_the_pad_and_a_slot_with_pp_is_on_it_over_an_open_list() {
|
||||||
// The live state row 34 came from: slot 0 is TACKLE with 0 of 40 PP and the cursor is on it.
|
// The live state row 34 came from: slot 0 is TACKLE with 0 of 40 PP and the cursor is on it.
|
||||||
|
|
@ -3823,6 +3937,15 @@ fn no_playable_scene_and_no_sub_state_deals_an_empty_pad() {
|
||||||
cornered.mons = vec![mon(0, 20, 20, &[])];
|
cornered.mons = vec![mon(0, 20, 20, &[])];
|
||||||
cornered.battle = Some((BattleKind::Trainer, true, false));
|
cornered.battle = Some((BattleKind::Trainer, true, false));
|
||||||
worst(&mut cornered);
|
worst(&mut cornered);
|
||||||
|
|
||||||
|
// And every battle sub-state of that same cornered turn, which since section 12.10 includes
|
||||||
|
// the bag: an empty bag deals `BACK` and nothing else, which is row 34a's answer -- there is
|
||||||
|
// nothing to choose, so leaving the list is the press, and what it leaves to is a menu with
|
||||||
|
// `MOVE 1` on it.
|
||||||
|
for list in [List::BattleMain, List::Moves(1), List::BattleParty, List::BattleBag] {
|
||||||
|
cornered.list = list;
|
||||||
|
worst(&mut cornered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Section 13.1's pad-empty rule: an outdoor map with every ledger against it still offers a walk.
|
/// Section 13.1's pad-empty rule: an outdoor map with every ledger against it still offers a walk.
|
||||||
|
|
|
||||||
|
|
@ -460,11 +460,13 @@ pub fn battle(memory: &mut dyn MemoryReader) -> Option<Battle> {
|
||||||
// frame is between turns, which is what it was before this change.
|
// frame is between turns, which is what it was before this change.
|
||||||
BattleMenu::Moves { cursor, .. } => cursor.is_some(),
|
BattleMenu::Moves { cursor, .. } => cursor.is_some(),
|
||||||
BattleMenu::Party { .. } => !forced_switch,
|
BattleMenu::Party { .. } => !forced_switch,
|
||||||
// The bag is a list the fly opened *during* its turn, but it is not the turn's own menu:
|
// The bag is a list the fly opened *during* its turn, and it is a menu cursor accepting
|
||||||
// the pad that belongs to it is the list's two answers, which is what the between-turns
|
// input, so by the rule above it is the fly's turn (2026-09-22, section 12.10). Reading it
|
||||||
// row deals (`NEXT`, `BACK`). Reporting it as the own turn would put the move buttons on
|
// as nobody's turn put it on the between-turns row, whose one button is the `NEXT` that
|
||||||
// a screen they cannot press.
|
// advances *text* -- and on an open bag that same A press *uses* whatever the cursor
|
||||||
BattleMenu::Bag { .. } => false,
|
// happens to be sitting on. The pad that belongs to a bag is the bag's own three answers,
|
||||||
|
// `ITEM`, `THROW BALL` and `BACK`, which is what `palette::scene_set` deals here now.
|
||||||
|
BattleMenu::Bag { .. } => true,
|
||||||
BattleMenu::None => false,
|
BattleMenu::None => false,
|
||||||
};
|
};
|
||||||
Some(Battle {
|
Some(Battle {
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,79 @@ fn a_forced_switch_is_the_party_list_that_cannot_be_cancelled() {
|
||||||
assert!(cursor(&mut wram).cancellable());
|
assert!(cursor(&mut wram).cancellable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Section 12.10: **a battle frame with a cursor accepting input is the fly's turn.**
|
||||||
|
///
|
||||||
|
/// The four menus a battle waits on are the top-level one, the move list, the party list and the
|
||||||
|
/// bag, and each of them is the game asking the player to choose. `own_turn` answered `false` for
|
||||||
|
/// the bag, which put it on the between-turns row whose one button is the `NEXT` that advances
|
||||||
|
/// text -- and on an open bag that same A press uses whatever the cursor holds.
|
||||||
|
///
|
||||||
|
/// The two frames that are correctly *not* the fly's turn are here too: a move list whose cursor
|
||||||
|
/// the seam cannot place (row 30b, a battle's opening frames) and a frame with no menu at all.
|
||||||
|
#[test]
|
||||||
|
fn a_battle_frame_with_a_cursor_accepting_input_is_the_flys_turn() {
|
||||||
|
let battler = |wram: &mut Wram| {
|
||||||
|
wram.party_mon(0, 4, 7, 14, 22, 0, &[(10, 35)]);
|
||||||
|
wram.party_mon(1, 16, 8, 24, 24, 0, &[(33, 35)]);
|
||||||
|
wram.battle_mon(0, 4, 7, 14, 22, 0, &[(10, 35), (45, 40), (33, 30)])
|
||||||
|
.enemy_mon(19, 3, 5, 11)
|
||||||
|
.battle(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The top-level menu.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
battler(&mut wram);
|
||||||
|
wram.battle_menu(false, 0);
|
||||||
|
assert!(battle(&mut wram).unwrap().own_turn, "the top-level menu");
|
||||||
|
|
||||||
|
// The move list, with a cursor the seam can place.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
battler(&mut wram);
|
||||||
|
wram.move_menu(1, 3);
|
||||||
|
assert!(battle(&mut wram).unwrap().own_turn, "the move list");
|
||||||
|
|
||||||
|
// The party list, chosen rather than forced.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
battler(&mut wram);
|
||||||
|
wram.party_list(1, false);
|
||||||
|
let fight = battle(&mut wram).unwrap();
|
||||||
|
assert!(!fight.forced_switch);
|
||||||
|
assert!(fight.own_turn, "the party list outside a forced switch");
|
||||||
|
|
||||||
|
// The bag, which `DisplayListMenuID` opens from the menu's ITEM entry. It is the one menu that
|
||||||
|
// read as nobody's turn, and it is what section 12.10 is about.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
battler(&mut wram);
|
||||||
|
wram.bag(&[(crate::pokemon_red::macros::cartridge::item::POTION, 2)]).set(ram::wListMenuID, poke::ITEM_LIST_MENU);
|
||||||
|
let fight = battle(&mut wram).unwrap();
|
||||||
|
assert_eq!(fight.menu, BattleMenu::Bag { cursor: 0, count: 1 });
|
||||||
|
assert!(fight.own_turn, "the battle bag is a cursor accepting input");
|
||||||
|
|
||||||
|
// And the two frames that are not a choice. A move list whose cursor cannot be placed is a
|
||||||
|
// battle's opening frames (row 30b), and no menu at all is text, an animation or a turn
|
||||||
|
// resolving.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
battler(&mut wram);
|
||||||
|
wram.move_menu(0, 3).set(ram::wCurrentMenuItem, 0);
|
||||||
|
let fight = battle(&mut wram).unwrap();
|
||||||
|
assert_eq!(fight.menu, BattleMenu::Moves { cursor: None, count: 3 });
|
||||||
|
assert!(!fight.own_turn, "a cursor the seam cannot place is not accepting input");
|
||||||
|
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
battler(&mut wram);
|
||||||
|
assert_eq!(battle(&mut wram).unwrap().menu, BattleMenu::None);
|
||||||
|
assert!(!battle(&mut wram).unwrap().own_turn, "no menu, no turn");
|
||||||
|
|
||||||
|
// A forced switch is a cursor accepting input and it is *not* the own turn, because it has a
|
||||||
|
// pad of its own: the exception 12.6 named, kept here so the invariant reads honestly.
|
||||||
|
let mut wram = Wram::overworld();
|
||||||
|
battler(&mut wram);
|
||||||
|
wram.party_list(1, true);
|
||||||
|
let fight = battle(&mut wram).unwrap();
|
||||||
|
assert!(fight.forced_switch);
|
||||||
|
assert!(!fight.own_turn, "a forced switch has its own pad");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_text_box_is_open_from_the_font_flag_and_waiting_from_the_box() {
|
fn a_text_box_is_open_from_the_font_flag_and_waiting_from_the_box() {
|
||||||
let mut wram = Wram::overworld();
|
let mut wram = Wram::overworld();
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,40 @@ struct Run {
|
||||||
/// The rung-9 trap: between turns there is nothing to back out of, so the press completes
|
/// The rung-9 trap: between turns there is nothing to back out of, so the press completes
|
||||||
/// where the fly stands and the turn does not move. `false` is the assertion.
|
/// where the fly stands and the turn does not move. `false` is the assertion.
|
||||||
back_without_a_list: bool,
|
back_without_a_list: bool,
|
||||||
|
/// Where a `BACK` or a `NEXT` on a battle frame was dealt, as `scene/sub-state`.
|
||||||
|
///
|
||||||
|
/// The bool above says a rule broke; this says on which frame, which is the difference
|
||||||
|
/// between a pad rule to fix and a scene the detector cannot name.
|
||||||
|
battle_back_where: std::collections::BTreeSet<String>,
|
||||||
|
battle_next_where: std::collections::BTreeSet<String>,
|
||||||
/// Whether `THROW BALL` ever started at a species the party already held (section 12.9).
|
/// Whether `THROW BALL` ever started at a species the party already held (section 12.9).
|
||||||
threw_at_a_held_species: bool,
|
threw_at_a_held_species: bool,
|
||||||
|
/// Whether one battle pad ever held both `NEXT` and `BACK` (section 12.10).
|
||||||
|
///
|
||||||
|
/// The pair is the trap: `NEXT` is the A that advances text, `BACK` is the B that leaves a
|
||||||
|
/// list, and a pad with both has two buttons that undo each other with nothing else changing.
|
||||||
|
next_and_back_on_one_pad: bool,
|
||||||
|
/// Whether `NEXT` was ever on the pad while a battle menu was accepting input.
|
||||||
|
///
|
||||||
|
/// The live v0.4.2 shape: `NEXT` on the top-level menu was an A press on FIGHT, so it opened
|
||||||
|
/// the move list that `BACK` closed again -- 1264 starts against 1241 in 71 hours.
|
||||||
|
next_on_a_menu_accepting_input: bool,
|
||||||
|
/// The longest chain of battle macro starts that alternated `NEXT`, `BACK`, `NEXT`, `BACK`.
|
||||||
|
///
|
||||||
|
/// Two is an accident of the rotation; the live log did it for seventy-one hours. Only the
|
||||||
|
/// previous battle start has to be kept to measure the chain.
|
||||||
|
longest_next_back_alternation: u32,
|
||||||
|
alternation: u32,
|
||||||
|
last_battle_start: Option<&'static str>,
|
||||||
|
/// Macros started inside the battle that is running, and the worst any *finished* battle cost.
|
||||||
|
///
|
||||||
|
/// The claim the rung-9 loop breaks is that a battle **ends**, and that it ends on a bounded
|
||||||
|
/// number of macros rather than on however many holds the 2-cycle takes to fall out of.
|
||||||
|
macros_this_battle: u32,
|
||||||
|
worst_battle_macros: u32,
|
||||||
|
battles_entered: u32,
|
||||||
|
battles_ended: u32,
|
||||||
|
was_in_battle: bool,
|
||||||
/// Maps on whose *overworld* pad `GO OBJECTIVE` was ever bound.
|
/// Maps on whose *overworld* pad `GO OBJECTIVE` was ever bound.
|
||||||
///
|
///
|
||||||
/// The objective is the road out: on rung 9 in the forest it has to be there, or the only way
|
/// The objective is the road out: on rung 9 in the forest it has to be there, or the only way
|
||||||
|
|
@ -256,7 +288,19 @@ impl Run {
|
||||||
move_button_on_battle_pad: false,
|
move_button_on_battle_pad: false,
|
||||||
battle_pad: None,
|
battle_pad: None,
|
||||||
back_without_a_list: false,
|
back_without_a_list: false,
|
||||||
|
battle_back_where: std::collections::BTreeSet::new(),
|
||||||
|
battle_next_where: std::collections::BTreeSet::new(),
|
||||||
threw_at_a_held_species: false,
|
threw_at_a_held_species: false,
|
||||||
|
next_and_back_on_one_pad: false,
|
||||||
|
next_on_a_menu_accepting_input: false,
|
||||||
|
longest_next_back_alternation: 0,
|
||||||
|
alternation: 0,
|
||||||
|
last_battle_start: None,
|
||||||
|
macros_this_battle: 0,
|
||||||
|
worst_battle_macros: 0,
|
||||||
|
battles_entered: 0,
|
||||||
|
battles_ended: 0,
|
||||||
|
was_in_battle: false,
|
||||||
objective_on_pad: std::collections::BTreeSet::new(),
|
objective_on_pad: std::collections::BTreeSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +376,19 @@ impl Run {
|
||||||
move_button_on_battle_pad: false,
|
move_button_on_battle_pad: false,
|
||||||
battle_pad: None,
|
battle_pad: None,
|
||||||
back_without_a_list: false,
|
back_without_a_list: false,
|
||||||
|
battle_back_where: std::collections::BTreeSet::new(),
|
||||||
|
battle_next_where: std::collections::BTreeSet::new(),
|
||||||
threw_at_a_held_species: false,
|
threw_at_a_held_species: false,
|
||||||
|
next_and_back_on_one_pad: false,
|
||||||
|
next_on_a_menu_accepting_input: false,
|
||||||
|
longest_next_back_alternation: 0,
|
||||||
|
alternation: 0,
|
||||||
|
last_battle_start: None,
|
||||||
|
macros_this_battle: 0,
|
||||||
|
worst_battle_macros: 0,
|
||||||
|
battles_entered: 0,
|
||||||
|
battles_ended: 0,
|
||||||
|
was_in_battle: false,
|
||||||
objective_on_pad: std::collections::BTreeSet::new(),
|
objective_on_pad: std::collections::BTreeSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -365,6 +421,37 @@ impl Run {
|
||||||
///
|
///
|
||||||
/// Section 12.9's question, through the same seam the macros read: `BACK` is a button where
|
/// Section 12.9's question, through the same seam the macros read: `BACK` is a button where
|
||||||
/// there is a list to leave and nowhere else.
|
/// there is a list to leave and nowhere else.
|
||||||
|
/// Whether the pad on this frame is a **battle** pad.
|
||||||
|
///
|
||||||
|
/// The scene the palette was dealt for, not `wIsInBattle`. The two differ on the `$ff` frame a
|
||||||
|
/// lost battle passes through and on a Safari or tutorial battle, where `state::battle` reads
|
||||||
|
/// nothing and `scene::detect` answers `Unknown` -- whose pad is `NEXT, BACK` by contract
|
||||||
|
/// (section 12.2, row 9: B is what leaves the Pokédex, the trainer card and OPTION). Asking
|
||||||
|
/// the cartridge byte instead accused that row of a battle rule it is not under.
|
||||||
|
fn battle_pad(&self) -> bool {
|
||||||
|
self.layer.scene_name() == "battle" || self.layer.scene_name() == "battle-switch"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The battle sub-state the seam reports, as a word, for the record and the failure message.
|
||||||
|
fn battle_sub_state(&mut self) -> &'static str {
|
||||||
|
use flybrain_gb::pokemon_red::macros::state::{BattleMenu, GameState};
|
||||||
|
let ledger = AdapterLedger(&self.adapter);
|
||||||
|
let mut state =
|
||||||
|
flybrain_gb::pokemon_red::state::PokeState::with_ledger(&mut self.gb, &ledger);
|
||||||
|
match state.battle() {
|
||||||
|
None => "no-battle",
|
||||||
|
Some(battle) => match battle.menu {
|
||||||
|
BattleMenu::Main { .. } => "main",
|
||||||
|
BattleMenu::Moves { cursor: Some(_), .. } => "moves",
|
||||||
|
BattleMenu::Moves { cursor: None, .. } => "moves-unplaceable",
|
||||||
|
BattleMenu::Party { .. } if battle.forced_switch => "party-forced",
|
||||||
|
BattleMenu::Party { .. } => "party",
|
||||||
|
BattleMenu::Bag { .. } => "bag",
|
||||||
|
BattleMenu::None => "between-turns",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn battle_list_open(&mut self) -> bool {
|
fn battle_list_open(&mut self) -> bool {
|
||||||
use flybrain_gb::pokemon_red::macros::state::{BattleMenu, GameState};
|
use flybrain_gb::pokemon_red::macros::state::{BattleMenu, GameState};
|
||||||
let ledger = AdapterLedger(&self.adapter);
|
let ledger = AdapterLedger(&self.adapter);
|
||||||
|
|
@ -483,9 +570,30 @@ impl Run {
|
||||||
}
|
}
|
||||||
// Section 12.9, on the cartridge: `BACK` belongs to a list. A battle frame with no list
|
// Section 12.9, on the cartridge: `BACK` belongs to a list. A battle frame with no list
|
||||||
// accepting input and `BACK` on the pad is the rung-9 trap itself.
|
// accepting input and `BACK` on the pad is the rung-9 trap itself.
|
||||||
if self.in_battle() != 0 && !self.battle_list_open() {
|
if self.battle_pad() {
|
||||||
self.back_without_a_list |=
|
let back = bound.iter().any(|channel| channel.as_str() == "macro_back");
|
||||||
bound.iter().any(|channel| channel.as_str() == "macro_back");
|
if back {
|
||||||
|
let where_ = format!("{}/{}", self.layer.scene_name(), self.battle_sub_state());
|
||||||
|
self.battle_back_where.insert(where_);
|
||||||
|
}
|
||||||
|
if back && !self.battle_list_open() {
|
||||||
|
self.back_without_a_list = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Section 12.10, on the cartridge: no battle pad holds a pair that undoes itself, and
|
||||||
|
// `NEXT` is on no frame with a cursor accepting input. The second is the stronger of the
|
||||||
|
// two -- the live pair was split across two sub-states, so no single pad held both.
|
||||||
|
if self.battle_pad() {
|
||||||
|
let next = bound.iter().any(|channel| channel.as_str() == "macro_next");
|
||||||
|
let back = bound.iter().any(|channel| channel.as_str() == "macro_back");
|
||||||
|
self.next_and_back_on_one_pad |= next && back;
|
||||||
|
if next {
|
||||||
|
let where_ = format!("{}/{}", self.layer.scene_name(), self.battle_sub_state());
|
||||||
|
self.battle_next_where.insert(where_);
|
||||||
|
}
|
||||||
|
if next && self.own_turn() {
|
||||||
|
self.next_on_a_menu_accepting_input = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// A facing window opens when `TALK`'s channel joins the pad and closes when it leaves.
|
// A facing window opens when `TALK`'s channel joins the pad and closes when it leaves.
|
||||||
let talk_bound = bound.iter().any(|channel| channel.ends_with("talk"));
|
let talk_bound = bound.iter().any(|channel| channel.ends_with("talk"));
|
||||||
|
|
@ -506,11 +614,32 @@ impl Run {
|
||||||
.collect();
|
.collect();
|
||||||
(decision.mask, started)
|
(decision.mask, started)
|
||||||
};
|
};
|
||||||
|
let in_battle_now = self.in_battle() != 0;
|
||||||
|
let on_a_battle_pad = self.battle_pad();
|
||||||
for name in started {
|
for name in started {
|
||||||
// Section 12.9's other half: a ball is never thrown at a species the party holds.
|
// Section 12.9's other half: a ball is never thrown at a species the party holds.
|
||||||
if name == "THROW BALL" && self.enemy_species_in_party() {
|
if name == "THROW BALL" && self.enemy_species_in_party() {
|
||||||
self.threw_at_a_held_species = true;
|
self.threw_at_a_held_species = true;
|
||||||
}
|
}
|
||||||
|
// Section 12.10's own signature, as the live event log printed it: `NEXT start/done,
|
||||||
|
// BACK start/done`, every hold, for seventy-one hours. Measured as the longest chain
|
||||||
|
// of consecutive battle starts drawn from those two alone and strictly alternating.
|
||||||
|
if in_battle_now {
|
||||||
|
self.macros_this_battle += 1;
|
||||||
|
}
|
||||||
|
if on_a_battle_pad {
|
||||||
|
let two = name == "NEXT" || name == "BACK";
|
||||||
|
self.alternation = match self.last_battle_start {
|
||||||
|
Some(last) if two && (last == "NEXT" || last == "BACK") && last != name => {
|
||||||
|
self.alternation.max(1) + 1
|
||||||
|
}
|
||||||
|
_ if two => 1,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
self.longest_next_back_alternation =
|
||||||
|
self.longest_next_back_alternation.max(self.alternation);
|
||||||
|
self.last_battle_start = Some(name);
|
||||||
|
}
|
||||||
*self.started.entry(name).or_insert(0) += 1;
|
*self.started.entry(name).or_insert(0) += 1;
|
||||||
}
|
}
|
||||||
self.gb.set_buttons(mask as u8);
|
self.gb.set_buttons(mask as u8);
|
||||||
|
|
@ -522,6 +651,24 @@ impl Run {
|
||||||
let ledger = AdapterLedger(&self.adapter);
|
let ledger = AdapterLedger(&self.adapter);
|
||||||
let _ = self.layer.observe(&mut self.gb, &ledger, ms);
|
let _ = self.layer.observe(&mut self.gb, &ledger, ms);
|
||||||
}
|
}
|
||||||
|
// Battle boundaries, after the frame: what a battle cost in macros, and whether it ended.
|
||||||
|
let now_in_battle = self.in_battle() != 0;
|
||||||
|
match (self.was_in_battle, now_in_battle) {
|
||||||
|
(false, true) => {
|
||||||
|
self.battles_entered += 1;
|
||||||
|
self.macros_this_battle = 0;
|
||||||
|
self.last_battle_start = None;
|
||||||
|
self.alternation = 0;
|
||||||
|
}
|
||||||
|
(true, false) => {
|
||||||
|
self.battles_ended += 1;
|
||||||
|
self.worst_battle_macros =
|
||||||
|
self.worst_battle_macros.max(self.macros_this_battle);
|
||||||
|
self.macros_this_battle = 0;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.was_in_battle = now_in_battle;
|
||||||
let map = self.map();
|
let map = self.map();
|
||||||
if self.route.last() != Some(&map) && map != u32::MAX {
|
if self.route.last() != Some(&map) && map != u32::MAX {
|
||||||
self.route.push(map);
|
self.route.push(map);
|
||||||
|
|
@ -744,7 +891,8 @@ fn forest_checkpoint() -> Option<flysim::store::Checkpoint> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From the rung-9 forest checkpoint: the turns advance, and `BACK` is never a battle's whole pad.
|
/// From the rung-9 forest checkpoint: the turns advance, the battles end, and no two buttons on a
|
||||||
|
/// battle pad undo each other.
|
||||||
///
|
///
|
||||||
/// **What was live** (2026-09-22, `infra/docs/macros-traps.md`): rank 9, VIRIDIAN FOREST, 69 hours
|
/// **What was live** (2026-09-22, `infra/docs/macros-traps.md`): rank 9, VIRIDIAN FOREST, 69 hours
|
||||||
/// on the rung, the ratchet's three attempts spent, and since the restart the macro starts were
|
/// on the rung, the ratchet's three attempts spent, and since the restart the macro starts were
|
||||||
|
|
@ -754,9 +902,18 @@ fn forest_checkpoint() -> Option<flysim::store::Checkpoint> {
|
||||||
/// `THROW BALL` spent balls on the species already in the party, each catch opening a nickname
|
/// `THROW BALL` spent balls on the species already in the party, each catch opening a nickname
|
||||||
/// screen the pad cannot leave.
|
/// screen the pad cannot leave.
|
||||||
///
|
///
|
||||||
/// The claim is about the *turn*, not about the fight: that a battle from this state ends, that the
|
/// **What was live again** (2026-09-22, thirty-five minutes after v0.4.2): the same rung, the same
|
||||||
/// fly's own presses are what ends it, and that neither of the two traps is on the pad any more.
|
/// forest, and the macro starts since the restart were `NEXT` 1264, `BACK` 1241, `THROW BALL` 5 and
|
||||||
/// Which move it picks and whether it wins are the fly's.
|
/// `GO WARP` 3, the event log alternating `NEXT start/done, BACK start/done` every hold. The pair
|
||||||
|
/// was split across two sub-states of one turn, so 12.9's rule held and the loop survived it:
|
||||||
|
/// `NEXT` on the top-level menu was an A press on FIGHT, which **opened** the move list, and `BACK`
|
||||||
|
/// on the move list **closed** it again. Section 12.10 takes `NEXT` off every pad with a cursor
|
||||||
|
/// accepting input and gives the bag its own; `MOVE 1` is the backstop the top-level menu keeps.
|
||||||
|
///
|
||||||
|
/// The claim is about the *turn*, not about the fight: that a battle from this state ends, that it
|
||||||
|
/// ends on a bounded number of macros, that the fly's own presses are what ends it, and that none
|
||||||
|
/// of the three traps is on the pad any more. Which move it picks and whether it wins are the
|
||||||
|
/// fly's.
|
||||||
///
|
///
|
||||||
/// ```sh
|
/// ```sh
|
||||||
/// FLY_ROM=/path/to/pokemon-red.gb \
|
/// FLY_ROM=/path/to/pokemon-red.gb \
|
||||||
|
|
@ -813,9 +970,34 @@ fn the_battles_turns_advance_from_the_rung_nine_forest_checkpoint() {
|
||||||
run.objective_on_pad
|
run.objective_on_pad
|
||||||
);
|
);
|
||||||
|
|
||||||
// The two traps, as assertions on the cartridge.
|
// The traps, as assertions on the cartridge.
|
||||||
assert!(!run.back_without_a_list, "`BACK` was on a battle pad with no list open");
|
eprintln!(
|
||||||
|
"`BACK` in a battle was dealt on {:?}; `NEXT` on {:?}",
|
||||||
|
run.battle_back_where, run.battle_next_where
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!run.back_without_a_list,
|
||||||
|
"`BACK` was on a battle pad with no list open: {:?}",
|
||||||
|
run.battle_back_where
|
||||||
|
);
|
||||||
assert!(!run.threw_at_a_held_species, "a ball was thrown at a species the party holds");
|
assert!(!run.threw_at_a_held_species, "a ball was thrown at a species the party holds");
|
||||||
|
// Section 12.10, the two halves of it.
|
||||||
|
assert!(
|
||||||
|
!run.next_and_back_on_one_pad,
|
||||||
|
"a battle pad held both `NEXT` and `BACK`: two buttons that undo each other"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!run.next_on_a_menu_accepting_input,
|
||||||
|
"`NEXT` was on the pad while a battle menu was accepting input, where A opens rather \
|
||||||
|
than advances"
|
||||||
|
);
|
||||||
|
// And the shape the log had, rather than only the pads it came from. Two in a row is the
|
||||||
|
// rotation happening to deal the pair; the live run did it for seventy-one hours.
|
||||||
|
assert!(
|
||||||
|
run.longest_next_back_alternation < 4,
|
||||||
|
"`NEXT`/`BACK` alternated {} times in a row",
|
||||||
|
run.longest_next_back_alternation
|
||||||
|
);
|
||||||
|
|
||||||
// The turn moves: the fly's own battle presses happen, and a battle this run entered or
|
// The turn moves: the fly's own battle presses happen, and a battle this run entered or
|
||||||
// resumed finishes.
|
// resumed finishes.
|
||||||
|
|
@ -828,6 +1010,33 @@ fn the_battles_turns_advance_from_the_rung_nine_forest_checkpoint() {
|
||||||
.sum();
|
.sum();
|
||||||
assert!(battle_presses > 0, "no move and no ball: {:?}", run.started);
|
assert!(battle_presses > 0, "no move and no ball: {:?}", run.started);
|
||||||
assert!(ended > 0, "no battle ever ended: {battles} entered");
|
assert!(ended > 0, "no battle ever ended: {battles} entered");
|
||||||
|
// **Every battle that started, finished**, and each one on a bounded number of macros. That is
|
||||||
|
// the claim the 2-cycle breaks, and the way it breaks it is the opposite of a slow fight: the
|
||||||
|
// battle never leaves the fly's own turn at all, so the count grows with the *run*. On v0.4.2
|
||||||
|
// from this same checkpoint the trap hunt spent all twenty of its brain minutes -- 71,673
|
||||||
|
// frames, 1,489 macros, 73 of 73 windows flagged -- inside **one** battle that never ended,
|
||||||
|
// with `BACK` 739 starts on the move list and `NEXT` 739 on the top-level menu. Four hundred
|
||||||
|
// is generous against the 275 this run's worst battle measured and far under an unbounded
|
||||||
|
// cycle.
|
||||||
|
assert!(
|
||||||
|
run.battles_ended + 1 >= run.battles_entered,
|
||||||
|
"{} battles entered and only {} left: a battle was entered and never got out",
|
||||||
|
run.battles_entered,
|
||||||
|
run.battles_ended
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
run.worst_battle_macros > 0 && run.worst_battle_macros < 400,
|
||||||
|
"the worst battle cost {} macros over {} that ended",
|
||||||
|
run.worst_battle_macros,
|
||||||
|
run.battles_ended
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"battles: {} entered, {} ended, worst {} macros; longest NEXT/BACK alternation {}",
|
||||||
|
run.battles_entered,
|
||||||
|
run.battles_ended,
|
||||||
|
run.worst_battle_macros,
|
||||||
|
run.longest_next_back_alternation
|
||||||
|
);
|
||||||
// `BACK` is still pressed, and that is the contract rather than a residual: over the move list
|
// `BACK` is still pressed, and that is the contract rather than a residual: over the move list
|
||||||
// and over a one-Pokemon party list it is one of the two answers a list has, and where it
|
// and over a one-Pokemon party list it is one of the two answers a list has, and where it
|
||||||
// leads is a menu with the move buttons on it (row 34). Its share is *reported* -- under this
|
// leads is a menu with the move buttons on it (row 34). Its share is *reported* -- under this
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue