From 029a3446fb3d033fcfcecf26b8bc783f269d9941 Mon Sep 17 00:00:00 2001 From: acamilo Date: Tue, 22 Sep 2026 08:06:19 +0000 Subject: [PATCH] macros: the move list deals BACK only where the moves can be read The v0.4.3 residual: BACK was 263 of 797 macro starts and every one of them was over an open move list. A move list whose battler the seam cannot place binds no MOVE n at all -- move_slot_bound needs wBattleMon* -- so its pad was BACK alone, and the only thing that button does is close the list MOVE 1 on the menu underneath had just opened. That is section 12.10's pair with MOVE 1 standing where NEXT used to. So BACK is dealt on the move list only while battle.own reads, and MOVE 1 keeps the backstop it has over the top-level menu: with nothing readable the pad is MOVE 1 alone and its script confirms wherever the cursor stands, which is the press that ends a turn. With the moves readable the pad is unchanged. --- .../src/pokemon_red/macros/palette.rs | 24 ++++++++++++++-- .../src/pokemon_red/macros/tests.rs | 28 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs index 1fc85e5..4cb61f1 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/palette.rs @@ -526,7 +526,20 @@ pub fn scene_set(scene: Scene, state: &mut dyn MacroState) -> Vec { // 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) { - BattleMenu::Moves { .. } => vec![Move1, Move2, Move3, Move4, Back], + // The move list. `BACK` is a button here because there is a list to leave (12.9) -- + // but only while the moves can be *read*: a battler the seam cannot place leaves all + // four `MOVE n` unbound, and a pad of `BACK` alone closes the list that `MOVE 1` on + // the menu underneath had just opened. That is 12.10's pair again, with `MOVE 1` in + // `NEXT`'s place. With nothing readable the pad is `MOVE 1` alone and its script + // confirms wherever the cursor stands, which is the press that ends the turn + // (section 12.11). + BattleMenu::Moves { .. } => { + if state.battle().and_then(|battle| battle.own).is_some() { + vec![Move1, Move2, Move3, Move4, Back] + } else { + vec![Move1] + } + } BattleMenu::Party { .. } => vec![Switch, Back], // The bag, which is the fly's turn since 12.10. Its three answers: use the thing the // cursor is on (`ITEM`), throw the ball (`THROW BALL`), or leave the list (`BACK`). @@ -1643,7 +1656,14 @@ pub fn move_slot_bound(state: &mut dyn MacroState, kind: MacroKind) -> bool { if index == 0 && matches!(battle.menu, BattleMenu::Main { .. }) { return true; } - let Some(own) = battle.own else { return false }; + // And the same backstop over an **open move list** whose battler the seam cannot read + // (section 12.11). That frame used to deal `BACK` alone -- the only button on it closed the + // list `MOVE 1` on the menu underneath had just opened, which is 12.10's pair with `MOVE 1` in + // `NEXT`'s place. `MOVE 1`'s script over an open list confirms wherever the cursor stands, so + // it reads no move either, and confirming a move is what ends a turn. + let Some(own) = battle.own else { + return index == 0 && matches!(battle.menu, BattleMenu::Moves { cursor: Some(_), .. }); + }; let holds = |slot: usize| -> Option<&Move> { own.moves.get(slot).and_then(|entry| entry.as_ref()).filter(|entry| entry.id != 0) }; diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs index 3a2bb60..33363db 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs @@ -4318,6 +4318,34 @@ fn an_overworld_pad_is_never_one_button_that_undoes_itself() { } } +/// Section 12.11: the move list deals `BACK` only where the moves can be read. +/// +/// The v0.4.3 residual (`infra/docs/macros-traps.md`): `BACK` was 263 of 797 macro starts, every +/// one over an open move list, and 142 of the run's `NEXT` starts were on a move list whose cursor +/// the seam could not place. A move list the seam cannot read the battler for binds no `MOVE n` at +/// all, so its pad was `BACK` alone -- and the only thing that button does is close the list that +/// `MOVE 1` on the menu underneath had just opened. That is 12.10's pair with `MOVE 1` in `NEXT`'s +/// place. `MOVE 1` alone confirms wherever the cursor stands, which is the press that ends a turn. +#[test] +fn the_move_list_deals_back_only_where_the_moves_can_be_read() { + let mut world = World::battle(); + world.list = List::Moves(3); + world.grid = false; + world.cursor_max = 2; + assert_eq!(pad_of(&mut world), ["BACK", "MOVE 1", "MOVE 2", "MOVE 3"]); + + // The battler the seam cannot place: no active slot, so `battle.own` is `None`. + world.active = None; + let pad = pad_of(&mut world); + assert_eq!(pad, ["MOVE 1"], "one button, and it ends the turn: {pad:?}"); + assert!(move_slot_bound(&mut world, MacroKind::Move1)); + assert!(!move_slot_bound(&mut world, MacroKind::Move2)); + // And it really presses: the cursor is confirmed where it stands, which is Struggle's own + // path (row 30a) and the only reading available here. + assert_eq!(run(&mut world, MacroKind::Move1).unwrap(), MacroAbort::Done); + assert_eq!(world.pulses.last(), Some(&buttons::A)); +} + /// Row 37 of `infra/docs/macros-traps.md`: a tile the cartridge pushes the fly off is not a tile /// to stand on, and the ground beside a villager is not the villager's fault. ///