From 4591e7907a6c0dfe4c92166ed88554287247ca0c Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 17:22:21 +0000 Subject: [PATCH] docs: macros.md 12.23, macros-wram.md section 13, the row 60 audit --- docs/design/macros-wram.md | 42 ++++++++++++++++++ docs/design/macros.md | 34 +++++++++++++++ infra/docs/macros-traps.md | 87 +++++++++++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 2 deletions(-) diff --git a/docs/design/macros-wram.md b/docs/design/macros-wram.md index 13d0ae8..04e2acd 100644 --- a/docs/design/macros-wram.md +++ b/docs/design/macros-wram.md @@ -953,3 +953,45 @@ where that layout puts them, so the byte is `wBattleType - 1` = `$d059`, asserte neighbours in `scene/tests.rs`. Measured on the cartridge in the Pewter Gym: `$00` in the overworld, `$cd` (`OPP_JR_TRAINER_M`) from the last box of the trainer's challenge through the 219-frame transition and the battle. `controllable` reads it as zero. + +## 13. What a move will do: the move table and the bytes its effect reads (2026-09-23, `docs/design/macros.md` 12.23) + +Row 60. Seven addresses, resolved by `tools/resolve_wram.py` and emitted into `symbols.rs`; the +tool now follows the decomp's `const` and `_RS` counters and a struct macro's field labels, which +is what reaches the `battle_struct` fields, and it re-derives 77 of 81 pinned addresses with no +disagreement. + +| symbol | address | what reads it | +| --- | --- | --- | +| `wPlayerMonStatMods` | `$cd1a` | six stages, ATTACK DEFENSE SPEED SPECIAL ACCURACY EVASION; 1 is -6, 7 normal, 13 is +6 | +| `wEnemyMonStatMods` | `$cd2e` | the same for the enemy | +| `wEnemyMonStatus` | `$cfe9` | the enemy's status byte (`battle_struct` +4) | +| `wEnemyMonType1` | `$cfea` | and `Type2` after it | +| `wEnemyMonAttack` | `$cff6` | the enemy's modified ATTACK DEFENSE SPEED SPECIAL, big-endian words | +| `wBattleMonAttack` | `$d025` | the same for the fly's Pokémon | +| `wEnemyBattleStatus2` | `$d068` | bit 1 Mist, 4 substitute, 5 must recharge | + +**The move table is ROM.** `Moves` opens `SECTION "Battle Engine 7"`, which `layout.link` places +first in bank `$0E`, so it is `$0E:$4000`, six bytes a row in move-id order from `POUND`: +animation (the move id itself), effect, power, type, accuracy, PP. `state::move_data` reads a row +through `MemoryReader::read_rom`, the cartridge image, and refuses a row whose first byte is not the +id asked for. Measured from the row-60 checkpoint: TACKLE (`$21`) effect `$00` power 35, TAIL +WHIP (`$27`) effect `$13` (`DEFENSE_DOWN1_EFFECT`) power 0. + +**`state::move_without_effect`** answers only refusals decided before the roll, for a move with no +power, from `engine/battle/effects.asm` and `MoveHitTest`: + +- `*_UP1` / `*_UP2`: the user's stage is 13, or (ATTACK..SPECIAL) the stat is 999; +- `*_DOWN1` / `*_DOWN2`: the target has Mist or a substitute, its stage is 1, or (ATTACK..SPECIAL) + the stat is 1 -- `StatModifierDownEffect` restores the stage and prints "Nothing happened!" + then, so a low-level target reaches it before -6; +- `SLEEP_EFFECT`: any status, unless the target must recharge; +- `POISON_EFFECT`: a substitute, any status, or a Poison type; +- `PARALYZE_EFFECT`: any status, or an Electric move against a Ground type. + +`None` outside a battle, without a cartridge, or with a stage byte outside 1..13. + +**Not covered** (the reading would be the same kind, and nothing early in the game reaches it): +Confuse Ray and Supersonic on a confused target, Leech Seed on a seeded or Grass target, Focus +Energy, Mist, Reflect and Light Screen already up, Disable on a disabled target, and a damaging +move the type chart makes "doesn't affect" (the chart is another ROM table). diff --git a/docs/design/macros.md b/docs/design/macros.md index 00fc5a8..8f7ef85 100644 --- a/docs/design/macros.md +++ b/docs/design/macros.md @@ -1505,6 +1505,40 @@ has a person to walk to where it had none, `GO OUT` is withheld by 12.5's own ru frames that were never the fly's deal nothing. The decoder, the reward catalog, the adapter version, the roles and the compatibility string are untouched. +### 12.23 A move the cartridge answers with nothing is not dealt beside one it does not (2026-09-23, row 60) + +Live on v0.5.5, early game after the reset to milestone 1: Route 1, Squirtle L5 (TACKLE, TAIL +WHIP) against a wild Pidgey, "Nothing happened!" on the screen. Since the reset `MOVE 2` 183 +times and `MOVE 1` once; the last reward 25 brain minutes before the checkpoint, one wild win in +the whole run. Check 10 flagged `unrewarded` (1,600 decisions, no reward event, no new ground on +two probes), which is right, and it is unchanged. + +- **The pad was at fault, not only the choice.** `MOVE n`'s precondition was "the slot holds a + move with PP", so TAIL WHIP stayed on the pad after it had walked the Pidgey's DEFENSE to the + point the cartridge refuses it. `StatModifierDownEffect` answers "Nothing happened!" when the + stage is already -6 **or the stat itself is already 1**, restoring the stage. The checkpoint is + the frame Squirtle fainted to a Pidgey L3 at DEFENSE -6 (stat 2); in the next battle the stat + reached 1 at -5. From there the pad dealt `MOVE 1, MOVE 2, RUN` and the fly pressed `MOVE 2` + until Squirtle fainted, woke at home and walked back: every battle lost, one wild win in the + run. The readout's favourite being `MOVE 2` is the fly's; a button that can do nothing at all + being on the pad is a macro that knows nothing about its own effect, section 12.2's trap. +- **What a move does is the cartridge's, read the same way for every move.** `state::move_data` + reads the move's row of `Moves` from the cartridge image (`$0E:$4000`, each row checked against + its own id) and `state::move_without_effect` answers the refusals the effect routines make on + bytes already in WRAM: a stat stage at its limit or a stat at 1 or 999, Mist or a substitute in + front of a stat-lowering move, a sleep, poison or paralysis move against a target that already has + a status, is Poison type, or is Ground type to an Electric move. No move is named; a miss is a + roll and is not answered. `macros-wram.md` section 13 has the bytes. +- **It is PP's rule.** A move the cartridge answers with nothing is not dealt beside one it does + not, exactly as a spent move is not (12.6, 12.8), and `MOVE 1` over the menu stops being FIGHT's + backstop only in that case. When no move would do anything the moves stay as PP deals them: + taking the last ones away would leave an open list whose only button is `BACK`, 12.11's pair, + and a turn that ends on "Nothing happened!" still ends. RUN, ITEM and SWITCH are untouched. + +Nothing is ranked, weighted or pressed for the fly: a button leaves the pad while it cannot change +anything and comes back when it can (a new battle resets the stages). The decoder, the reward +catalog, the adapter version, the roles and the compatibility string are untouched. + ## 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 ## for the Pokécenter. heal should be a macro.") diff --git a/infra/docs/macros-traps.md b/infra/docs/macros-traps.md index f711144..3f464b9 100644 --- a/infra/docs/macros-traps.md +++ b/infra/docs/macros-traps.md @@ -2757,5 +2757,88 @@ route survey above is the reproduction; the hunt is reported, not smoothed. - `cargo clippy --all-targets`: **0 warnings**. - `npm test` 663 passed; `npm run typecheck` clean. - `infra/tests/lint.sh`: ALL CHECKS PASSED, check 10's two new cases and the de-PII guard included. -- `flysim --print-compatibility`: **648 bytes, sha256 `4929f340...9ebd9`**, byte-identical to the - base. Decoder, reward catalog, adapter version and roles untouched. +- `flysim --print-compatibility`: byte-identical to the base on both bases this branch has had: + 648 bytes, sha256 `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68` + on `main` after the engagement rewards (adapter v7). Decoder, reward catalog, adapter version + and roles untouched. + +## 2026-09-23, row 60: TAIL WHIP after it stopped working + +### What was live + +Map 12 (Route 1), rank 9, v0.5.5, after the operator reset the run to milestone 1: Squirtle L5 +(TACKLE, TAIL WHIP) in wild battles with Pidgey, "Nothing happened!" on the screen. Since the reset +`MOVE 2` 183 starts and `MOVE 1` one; the last reward 25 brain minutes before the checkpoint, +1,600 decisions since, one wild win in the whole run. Check 10 flagged `unrewarded` -- correctly: +100+ decisions, no reward event, no new ground on two probes. It is unchanged. + +### The survey: the pad on the fly's own turn + +The route probe from the checkpoint (`FLY_PROBE_CATCH=route`, `FLY_PROBE_PREFER="MOVE 2"`, the +live readout's favourite; 72,000 frames) now prints the battle bytes on every pad change. The +checkpoint is the frame Squirtle fainted: Pidgey L3, DEFENSE stage 1 (-6), DEFENSE 2, TAIL WHIP's +row `$27`: effect `$13` power 0. In the next battle DEFENSE reached **1 at stage -5**, which +`StatModifierDownEffect` refuses as well. From then on the own-turn pad was `MOVE 1, MOVE 2, RUN` +over the menu and `BACK, MOVE 1, MOVE 2` over the list: **every own-turn pad with TAIL WHIP +refused dealt `MOVE 2`** (72 of 72; a 73rd frame was a list with no placeable cursor, `NEXT` +alone), TACKLE beside it every time. `MOVE 1` was never missing; `MOVE 2` was a +button that could change nothing. + +| # | trap | trigger | test | fix, or why it is left | +| --- | --- | --- | --- | --- | +| 60 | `MOVE n` is bound by PP alone, so a move the cartridge refuses ("Nothing happened!", "didn't affect") stays on the pad beside one that works | any stat move at its stage or stat limit, any status move at a target it cannot affect; Route 1, TAIL WHIP at a Pidgey's DEFENSE 1, every battle lost | `a_move_the_cartridge_answers_with_nothing_is_off_the_pad_beside_one_it_does_not`, `a_spent_move_and_a_move_without_effect_leave_the_one_that_works`, `with_no_move_that_does_anything_the_moves_stay_as_pp_deals_them`, six `state` tests, `tail_whip_at_its_limit_is_not_dealt_and_a_route_one_battle_is_won` (ROM) | **fixed**: `state::move_data` reads the move's row of `Moves` (`$0E:$4000`) from the cartridge image; `state::move_without_effect` answers the refusals decided before the roll (stage 1/13, stat 1/999, Mist or substitute, a statused, Poison or Ground target); the palette treats such a move as it treats a spent one. `docs/design/macros.md` 12.23, `macros-wram.md` section 13 | + +### Before and after + +The ROM-gated run, 72,000 frames (20.1 brain minutes) from the checkpoint, the real palette +driven with `MOVE 2` preferred and a uniform choice otherwise; base is this branch with the +palette commit reverted: + +| measure | base | branch | +| --- | ---: | ---: | +| own-turn frames with a refused move beside a useful one | 2,657 | 568 | +| ... of them dealing the refused move | **2,657** | **0** | +| battles ended / won | 12 / **0** | 14 / **3** | +| battles ended with the fly's Pokémon fainted | 12 | 8 | +| battle length, frames, median / max | 4,970 / 8,994 | 3,594 / 4,793 | +| `MOVE 2` / `MOVE 1` / `RUN` starts | 94 / 0 / 0 | 56 / 5 / 4 | + +The route survey, same driver and checkpoint: base never leaves Pallet Town, Red's house and +Route 1 (637 tiles at the end); the branch reaches Viridian City, its Pokémon Center and mart and +Route 2 (748). + +The survey itself, same driver: own-turn pads dealing a refused TAIL WHIP 72 -> **0** (of 11 at +the limit); `MOVE 2` / `MOVE 1` / `RUN` done 94 / 0 / 0 -> 56 / 4 / 4; maps with a macro done 4 -> 10. + +**The real-brain trap hunt was not run to the end.** Both 20-minute arms (`trap_hunt` now reports +payouts by kind, battle lengths and wins, and `MOVE n` starts on a move without effect) were +started from the checkpoint and stopped after 1 h 48 min wall at about 19 CPU-minutes each: the box +sat at load 25-40 and a stub arm ticks the same brain. The ROM-gated run and the survey above are +the before/after; they drive the real palette with the live readout's measured preference instead +of the brain, which is the deviation. + +### Residuals, named rather than worked around + +- **The fly still spends TAIL WHIP while it works.** Six presses at stage 7 to 1 are the fly's + choice, and a Squirtle at 8/20 can faint doing it; eight of fourteen battles on the branch + still ended that way. What changed is that the seventh is not on the pad. +- **Not covered, same kind, nothing early reaches it:** Confuse Ray and Supersonic on a confused + target, Leech Seed on a seeded or Grass target, Focus Energy, Mist, Reflect and Light Screen + already up, Disable on a disabled target, and a damaging move the type chart makes "doesn't + affect" (the chart is another ROM table). `macros-wram.md` section 13. +- **With no move that would do anything, the moves stay as PP deals them.** Taking them away would + leave an open list with `BACK` alone (12.11); a turn that ends on "Nothing happened!" still ends. + +### Gates + +- `cargo test --release -p flybrain-gb` with `FLY_ROM`: 426 + 27 passed, 0 failed. +- `cargo test --release -p flysim --no-fail-fast` with `FLY_ROM` and the row-60 checkpoint: + all passed but `integration::the_service_streams_takes_sugar_checkpoints_and_resumes_after_being_killed`, + the known load failure (`total` 1 against 38, or the feed at 14-18 Hz, on a box at load 30), + failing the same two ways on the base. +- `cargo clippy --workspace --all-targets -- -D warnings`: clean. +- `npm test` 663 passed; `npm run typecheck` clean; `infra/tests/lint.sh` ALL CHECKS PASSED. +- `flysim --print-compatibility`: byte-identical to the base on both bases this branch has had: + 648 bytes, sha256 `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68` + on `main` after the engagement rewards (adapter v7). Decoder, reward catalog, adapter version + and roles untouched.