diff --git a/docs/design/macros.md b/docs/design/macros.md index f7a9052..74bb2be 100644 --- a/docs/design/macros.md +++ b/docs/design/macros.md @@ -1608,6 +1608,48 @@ split map has the pieces its ground has, and a frame that was the cartridge's is fly's. The decoder, the reward catalog, the adapter version, the roles and the compatibility string are untouched. +### 12.25 A trainer's challenge is the cartridge's until its battle is over (2026-09-23, row 61) + +Live on v0.5.5, rung 9, for twenty minutes: `GO OBJECTIVE` into Viridian Forest's south gate (map +50, `$32`), `GO OUT` straight back onto Route 2, `GO WARP` back from the forest, `GO OBJECTIVE +blocked` in the forest, no reward. Reproduced with the route survey from the live checkpoint +(uniform choice per hold, xorshift seed 7), which walks the same ring for twenty brain minutes. + +- **The ring's cause was a wall, not the gate.** The forest's only road to its north gate is a + two-wide corridor at x = 1-2; a Bug Catcher stands on (2, 18) facing west. A walk up the + corridor steps onto (1, 18), the trainer takes the joypad, and row 58's held push-back waits for + the joypad to come back. `DisplayEnemyTrainerTextAndStartBattle` clears `wJoyIgnore` before the + challenge text and `StartTrainerBattle` writes `wCurOpponent` only after that text's close-down: + **five frames** with no box, no script bit and `wCurOpponent` zero, which the seam read as the + fly's overworld. The push was written there; (1, 18) went into the pushed ledger, which has no + window, and from then on every walk to the north gate had no road. `GO OBJECTIVE` walked to the + nearest reachable tile, a dead end at (6, 1), and was blocked; `GO WARP`'s last tier took the + south gate, whose `GO OUT` is Route 2, whose `GO OBJECTIVE` is the gate. +- **The fact is `wStatusFlags7` bit 3, `BIT_TRAINER_BATTLE`**: set by `CheckFightingMapTrainers` + on the "!", cleared at `.battleOccurred` after every battle (before the blackout check). It + covers more than the five frames: the "!" bubble runs about sixty frames before `wJoyIgnore` is + set, and they read as the fly's overworld too -- about sixty-six free-looking frames per + engagement, measured. An overworld frame with the bit set is `Unknown` in the macros' own + scene, with no text box, so the pad is empty, no ground is recorded and no held entry is + decided on it. A trainer talked to by the fly never sets it; its `wCurOpponent` is written + inside the text. +- **On main the five frames are already row 59's** (12.24: a held push-back is written only after + thirty frames of overworld), and that alone keeps (1, 18) clear. This row is the cartridge-fact + layer under it: the pad is empty through the bubble as well, no ground is recorded, and it does + not depend on the gap staying under thirty frames. +- **The macros' reading only.** `controllable` and `scene::detect` are shared with the reward + adapter and do not change; `PokeState`'s `scene` and `scripted` read the bit beside them. In + macros mode the feed's `game.scene` is the palette's, so it reads `unknown` on those frames, + which is what the contract says of a frame the cartridge is driving. +- **The gates were modelled right.** Both forest gates are on the graph and `next_hop` answers + the forest from the south gate and Route 2 from the north one. The south gate's `GO OUT` is the + "a room has to be leavable" tier, a way back that is the fly's choice: with the corridor open, + the survey seed that walked into the gate twelve times still earned the badge, on both arms. + +Nothing is ranked or pressed for the fly: 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. + ## 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 986c862..04d4d28 100644 --- a/infra/docs/macros-traps.md +++ b/infra/docs/macros-traps.md @@ -2967,3 +2967,87 @@ survey and the three ROM tests, which is a deviation from the ethos check's lett - `flysim --print-compatibility`: byte-identical to the base on both bases this branch has had: 648 bytes, `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68` on `4d82f7d` (v7). Decoder, reward catalog, adapter version and roles untouched. + +## 2026-09-23, row 61: the forest's south gate, in and out + +### What was live + +Map 50 (`VIRIDIAN_FOREST_SOUTH_GATE`, `$32`) and Route 2's south half, rung 9, v0.5.5, about 20:45 +to 21:05 UTC: `GO OBJECTIVE` into the gate (0.3 s), `GO OUT` back onto Route 2 (0.2 s), `GO WARP` +back out of the forest, `GO OBJECTIVE blocked` in the forest, `GO ROUTE` refused now and then; no +reward and about eight new tiles in twenty minutes. The checkpoint is the fly at (5, 1) in the +gate, under the forest's doorway, objective Pewter City; `next_hop` answers the forest, correctly. + +### The survey: what walled the road + +The route survey from the checkpoint (`FLY_PROBE_CATCH=route`, uniform choice per hold, +`FLY_PROBE_RNG=7`, 72,000 frames) walks the live ring and never leaves rung 9. Caught at the +refusal (`FLY_PROBE_CATCH_FRAME`, `FLY_PROBE_WHOLE=1`): the forest's only road to the north gate +is the corridor at x = 1-2 from row 22 up to row 0; the Bug Catcher of +`EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2` stands on (2, 18) facing west, and **(1, 18) was in the +pushed ledger**. It was written at frame 16,389: a `GO ITEM` walk stepped onto (1, 18), the trainer +took the joypad, and his text closed onto five frames with no box, no script bit and +`wCurOpponent` zero (`StartTrainerBattle` runs after `DisplayTextID`'s close-down). Row 58's held +entry was decided on the first of them. The "!" bubble before it, about sixty frames before +`wJoyIgnore` is set, reads the same way: about sixty-six free-looking frames per engagement. The pushed ledger has no window, so the north gate had no +road for the session; `GO OBJECTIVE` walked to the nearest reachable tile, (6, 1), a dead end, and +was blocked, and the last tiers walked the fly back to the south gate and Route 2. Every base +survey arm walls (1, 18); only a fly that has to come back up the corridor is trapped by it. + +| # | trap | trigger | test | fix, or why it is left | +| --- | --- | --- | --- | --- | +| 61 | the "!" bubble and the frames between a sighted trainer's challenge text and `StartTrainerBattle` (about 66 per engagement) read as the fly's overworld: a pad is dealt, ground recorded, and row 58's held push-back written, walling the tile the trainer fired on for the session | any trainer whose line of sight a walk crosses; Viridian Forest's (1, 18), the one free tile of the corridor to the north gate | `a_trainers_challenge_is_the_cartridges_until_its_battle_is_over`, `the_frames_between_a_trainers_text_and_its_battle_deal_no_pad_and_record_no_ground`, `a_trainers_challenge_does_not_wall_the_road_to_the_forests_north_gate` (ROM) | **fixed** in the macro seam: `state::trainer_engaged` reads `wStatusFlags7` bit 3 (`BIT_TRAINER_BATTLE`, set by `CheckFightingMapTrainers`, cleared at `.battleOccurred`); `PokeState`'s `scene` is `Unknown` on an overworld frame with it set and `scripted` is true. `controllable` and `scene::detect` unchanged; in macros mode `game.scene` reads `unknown` on those frames. `docs/design/macros.md` 12.25 | +| 61b | the south gate deals `GO OUT` (back to Route 2) beside the forest door the objective's road takes | the "a room has to be leavable" tier in a gate whose way on is a passage | -- | **left**: a way back is the fly's choice; with the corridor open no survey stays on it | + +### Before and after + +The route survey, 72,000 frames (20.1 brain minutes), base `main` vs branch: + +| driver | base | branch | +| --- | --- | --- | +| uniform, seed 7 (the live ring) | rung 9, 1,202 tiles, north gate never, (1, 18) walled | **BOULDER BADGE**, 1,647 tiles, north gate f20273, Pewter f23756 | +| uniform, default seed | rung 10, 1,356 | rung 11, 1,476 | +| `GO OBJECTIVE` preferred | rung 11, 1,529 | rung 12, 1,492 | +| `GO OUT`, `GO OBJECTIVE` preferred | rung 11, 1,376 | rung 11, 1,446 | +| uniform, seed 12345 | rung 11, 1,765 | identical | + +(1, 18) is walled on every base arm and on no branch arm. The ROM-gated run (the seed-7 driver, +40,000 frames): base walled at frame 16,389, rung 9, **fails**; branch: north gate f20272, Route 2 +f21209, Pewter f23755, passes. + +The stub-readout trap hunt (30 brain minutes, service frame since FND-01; `main` `4d82f7d` vs +branch): both arms rung 10, 362 distinct tiles, 147 macros all done, 11 of 113 windows flagged. +**The stub does not walk the ring**: it passes the corridor northward and never comes back, so +the only difference is 134 overworld frames the branch reads `Unknown`, the trainer gaps. The +survey and the ROM test are the reproduction. + +### Other gates + +The north gate's `GO OUT` is toward Pewter (tier 2) and its forest door is withheld; the Route 2 +gate, Diglett's Cave's Route 2 house and the Route 22 gate are off the graph (every door `LAST_MAP` +but Diglett's passage). The Route 22 gate is the one building with `LAST_MAP` on both sides of two +different maps (its script sets `wLastMap` by row: under 4 is Route 23); `outdoor_of` cannot name +both and will need a per-door answer when a rung routes through it. The trainer gap is not a gate +fact: every sighted trainer has it. + +### Overlap + +Row 59 (59f, merged in v0.6.1) found the same five frames on Route 3 and holds the push until +thirty frames of overworld; on main that alone keeps (1, 18) clear. This row is the cartridge-fact +layer under it: the pad is empty through the bubble too, no ground is recorded, and it does not +depend on the gap staying under thirty frames. The ROM test asserts no pad on an overworld frame +with the bit set, which row 59 alone does not meet. + +### Gates + +On `main` `510727c` (v0.6.1, row 59 merged): + +- `cargo test --release -p flybrain-gb` with `FLY_ROM`: 461 passed, 0 failed. +- `rom_macros_mode` with the row-61 and row-59 checkpoints: row 61's test and row 59's three pass. + Row 61's test on the branch with `trainer_engaged` neutered (main's behaviour, row 59's settle + alone): the wall stays clear, and a pad is dealt on 67 of 67 challenge frames -- **fails**; on + the branch 0 of 67 -- passes. +- `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`, raw and macros: 648 bytes, sha256 `8ce67b97...a8f68`, the same + as `main`. Decoder, reward catalog, adapter version and roles untouched. diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs index eca338d..23f9fa6 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/driver.rs @@ -616,6 +616,28 @@ mod tests { assert_eq!(palette.observe(&mut wram, &NoLedger).scene, SceneId::Unknown); } + #[test] + fn the_frames_between_a_trainers_text_and_its_battle_deal_no_pad_and_record_no_ground() { + // Row 61, Viridian Forest. A trainer who saw the fly: its "!" bubble and the five frames + // after its challenge text read as an ordinary overworld -- no box, no script bit, + // `wJoyIgnore` and `wCurOpponent` zero. The pad was dealt there and the push-back the + // fly's walk had earned when the trainer took the joypad was written there, walling the + // one free tile of the corridor to the north gate for the session. + let mut wram = Wram::overworld(); + wram.set(crate::pokemon_red::symbols::ram::wStatusFlags7, 1 << 3); + let mut palette = PokemonPalette::new(7); + let engaged = palette.observe(&mut wram, &NoLedger); + assert_eq!(engaged.scene, SceneId::Unknown, "the cartridge's, inside the challenge"); + assert!(engaged.bindings.is_empty(), "nothing to press: {:?}", engaged.bindings); + assert_eq!(palette.stood(), 0, "and no ground recorded from it"); + + // `.battleOccurred` clears the bit and the overworld is the fly's again. + wram.set(crate::pokemon_red::symbols::ram::wStatusFlags7, 0); + let own = palette.observe(&mut wram, &NoLedger); + assert_eq!(own.scene, SceneId::Overworld); + assert_eq!(palette.stood(), 1); + } + #[test] fn a_teleport_pad_is_not_a_tear() { // Saffron Gym and two Silph Co. floors warp to themselves. Standing on a pad whose diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs index af6a2d8..f5279ea 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs @@ -145,6 +145,11 @@ pub mod poke { /// zero in the overworld, non-zero from the frame a trainer's challenge closes to the end of /// the battle, including the 219 frames of the battle transition in between. pub const CUR_OPPONENT: u16 = super::ram::wBattleType - 1; + /// `constants/ram_constants.asm`: `wStatusFlags7` bit 3, `BIT_TRAINER_BATTLE` (row 61). Set by + /// `CheckFightingMapTrainers` (`home/trainers.asm`) on the frame a trainer sees the player, + /// cleared at `.battleOccurred` (`home/overworld.asm`) once the battle is over -- before the + /// blackout check, so a lost battle clears it too. Nothing else writes it. + pub const TRAINER_BATTLE_STATUS7: u8 = 1 << 3; /// `constants/battle_constants.asm`: the non-volatile status byte. pub const SLP_MASK: u8 = 0b111; @@ -1841,8 +1846,14 @@ impl<'a> PokeState<'a> { } impl GameState for PokeState<'_> { + /// [`super::scene::detect`], except that an overworld frame inside a trainer's challenge is + /// the cartridge's ([`trainer_engaged`], row 61): section 12.13's `Unknown` with no text box, + /// an empty pad the fly waits out, and no frame a held push-back is decided on. fn scene(&mut self) -> Scene { - super::scene::detect(self.memory) + match super::scene::detect(self.memory) { + Scene::Overworld if trainer_engaged(self.memory) => Scene::Unknown, + scene => scene, + } } fn player(&mut self) -> Option { @@ -1910,6 +1921,27 @@ impl GameState for PokeState<'_> { } } +/// Whether a trainer who saw the player is between its "!" and the end of its battle (row 61). +/// +/// Two stretches of that window read as an overworld the fly owned, both measured in Viridian +/// Forest: the "!" bubble, about sixty frames, drawn before `CheckFightingMapTrainers` sets +/// `wJoyIgnore`; and five frames after the challenge text, because +/// `DisplayEnemyTrainerTextAndStartBattle` (`home/trainers.asm`) clears `wJoyIgnore` before the +/// text and calls `StartTrainerBattle`, which writes `wCurOpponent`, only after the text's +/// close-down has redrawn the map. About sixty-six frames per engagement, every bit +/// [`controllable`] reads clear. The push-back a walk earned when the trainer took the joypad (row +/// 58's held entry) was written on the first frame after the text: the one free tile beside the +/// trainer, in the only corridor to the forest's north gate, walled for the session. +/// +/// **The macros' reading only.** [`controllable`] and [`super::scene::detect`] are shared with +/// the reward adapter (the talk payout's "ready" test) and do not change; [`PokeState`]'s own +/// `scene` and `scripted` read this beside them. In macros mode the feed's `game.scene` is the +/// palette's, so it reads `unknown` on these frames, as the contract has it for a frame the +/// cartridge is driving. +pub fn trainer_engaged(memory: &mut dyn MemoryReader) -> bool { + read(memory, ram::wStatusFlags7) & poke::TRAINER_BATTLE_STATUS7 != 0 +} + /// The cartridge tables on their defaults, and the exploration ledger wired through. /// /// `pokemon_red/macros/cartridge.rs` defaults every [`MacroState`] method and every default @@ -1923,7 +1955,7 @@ impl GameState for PokeState<'_> { /// taken rather than at the nearest door (`docs/design/macros.md` section 3). impl MacroState for PokeState<'_> { fn scripted(&mut self) -> bool { - !controllable(self.memory) + !controllable(self.memory) || trainer_engaged(self.memory) } fn text_open(&mut self) -> bool { diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/state/tests.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/state/tests.rs index 751915c..5c1dbce 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/state/tests.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/state/tests.rs @@ -1222,3 +1222,34 @@ fn a_refusal_that_cannot_be_read_is_not_reported() { read.set(ram::wEnemyMonStatMods + 1, 1); assert!(PokeState::new(&mut read).move_without_effect(TAIL_WHIP.0)); } + +#[test] +fn a_trainers_challenge_is_the_cartridges_until_its_battle_is_over() { + use crate::pokemon_red::macros::state::GameState; + // Row 61. A trainer who saw the fly: its "!" bubble (before `wJoyIgnore` is set) and the five + // frames after its text (before `wCurOpponent` is) have every bit `controllable` reads clear. + // The macros read them as the cartridge's; the shared readings the reward adapter uses do not + // move. + let mut wram = Wram::overworld(); + assert_eq!(PokeState::new(&mut wram).scene(), Scene::Overworld); + assert!(!PokeState::new(&mut wram).scripted()); + + wram.set(ram::wStatusFlags7, poke::TRAINER_BATTLE_STATUS7); + assert!(trainer_engaged(&mut wram)); + assert_eq!(PokeState::new(&mut wram).scene(), Scene::Unknown, "inside the challenge"); + assert!(PokeState::new(&mut wram).scripted(), "and the fly is not its own master"); + assert!(controllable(&mut wram), "the adapter's gate is unchanged"); + assert_eq!(crate::pokemon_red::scene::detect(&mut wram), Scene::Overworld, "and the shared scene"); + + // The challenge's own text is still a conversation to advance. + wram.dialogue_box(); + assert_eq!(PokeState::new(&mut wram).scene(), Scene::Dialog); + + // `.battleOccurred` clears the bit; the other bits of the byte are not a challenge: + // `BIT_NO_MAP_MUSIC` after a rival, `BIT_USE_CUR_MAP_SCRIPT` from a trainer talked to. + let mut after = Wram::overworld(); + after.set(ram::wStatusFlags7, (1 << 1) | (1 << 4) | (1 << 7)); + assert!(!trainer_engaged(&mut after)); + assert_eq!(PokeState::new(&mut after).scene(), Scene::Overworld); + assert!(!PokeState::new(&mut after).scripted()); +} diff --git a/services/flysim/crates/flysim/examples/scene_probe.rs b/services/flysim/crates/flysim/examples/scene_probe.rs index 98d4d9a..61fbcf8 100644 --- a/services/flysim/crates/flysim/examples/scene_probe.rs +++ b/services/flysim/crates/flysim/examples/scene_probe.rs @@ -1389,6 +1389,10 @@ fn route_survey( let save_rank: Option = std::env::var("FLY_PROBE_SAVE_RANK").ok().and_then(|value| value.parse().ok()); let mut saved = false; + // `FLY_PROBE_CATCH_FRAME=41600` reads the first frame at or after that one on which the fly + // has the buttons and no macro is running (row 61: the frame a walk in the forest refused). + let catch_frame: Option = + std::env::var("FLY_PROBE_CATCH_FRAME").ok().and_then(|value| value.parse().ok()); // `FLY_PROBE_HOLD=right:96,up:32` holds raw directions first and prints where the fly is // every eight frames: what the cartridge does with a press, before any macro is asked. @@ -1543,6 +1547,14 @@ fn route_survey( caught_at = Some(frame); break; } + if catch_frame.is_some_and(|at| frame >= at) + && !running + && matches!(observed.scene, flybrain_gb::SceneId::Overworld) + && !observed.bindings.is_empty() + { + caught_at = Some(frame); + break; + } if let (Some(want), Some(player)) = (catch_map, player) { if player.map == want && last_map != Some(want) { entries += 1; @@ -1581,6 +1593,8 @@ fn route_survey( frame as f64 * MS_PER_FRAME / 60_000.0, if single_refusals >= catch_after { "one button, refused twenty holds running".to_string() + } else if catch_frame.is_some_and(|at| frame >= at) { + format!("the first free frame at or after {catch_frame:?}") } else { format!("arrival {entries} on map {catch_map:?}") } @@ -1634,7 +1648,9 @@ fn route_survey( // A room small enough to print whole is printed whole, with its people on it (row 58: // the gym's leader is twelve rows from the door). let size = state.map_size().expect("a loaded map"); - let whole = size.width <= 24 && size.height <= 24; + // `FLY_PROBE_WHOLE=1` prints a bigger map whole too (row 61: the forest is 34 by 48). + let whole = (size.width <= 24 && size.height <= 24) + || std::env::var("FLY_PROBE_WHOLE").is_ok_and(|value| value == "1"); let people: Vec<(Tile, TalkTarget)> = path::person_targets(state) .into_iter() .chain(path::offscreen_person_targets(state)) diff --git a/services/flysim/crates/flysim/tests/rom_macros_mode.rs b/services/flysim/crates/flysim/tests/rom_macros_mode.rs index 1418ac9..38ba60e 100644 --- a/services/flysim/crates/flysim/tests/rom_macros_mode.rs +++ b/services/flysim/crates/flysim/tests/rom_macros_mode.rs @@ -3661,3 +3661,148 @@ fn the_fly_goes_into_mt_moon_from_the_live_route_4_checkpoint() { assert!(crossings < 25, "{crossings} crossings of Route 4's west doors: {:?}", run.started); assert!(in_mt_moon.is_some(), "the fly never went into Mt. Moon: {:?}", run.route); } + +fn row61_checkpoint() -> Option { + std::env::var_os("FLY_ROW61_CHECKPOINT").map(|path| { + flysim::store::load(std::path::Path::new(&path)) + .expect("the checkpoint should be a FLYSIM01 envelope") + }) +} + +/// Viridian Forest's south gate, from the checkpoint pulled during the ring. +/// +/// **What was live** (2026-09-23, v0.5.5, rung 9 after the reset to milestone 1): for twenty +/// minutes `GO OBJECTIVE` into the forest's south gate and `GO OUT` straight back onto Route 2, +/// `GO WARP` back from the forest, `GO OBJECTIVE blocked` in the forest, no reward +/// (`infra/docs/macros-traps.md` row 61). The forest's only road to its north gate is a two-wide +/// corridor at x = 1-2 with a Bug Catcher standing on (2, 18) facing west. A walk up the corridor +/// steps onto (1, 18) and the trainer takes the joypad. His "!" bubble (about sixty frames, before +/// `wJoyIgnore` is set) and the five frames after his text (before `wCurOpponent` is) read as the +/// fly's own overworld: the push-back the walk had earned was written on the first frame after the +/// text, (1, 18) was walled for the session, and every later walk to the north gate had no road. +/// Row 59's thirty-frame settle alone keeps the wall out; this row reads `BIT_TRAINER_BATTLE`. +/// +/// The driver is the route survey's: the real palette, one uniform choice per hold, xorshift +/// seeded 7 -- a harness choice, not the fly's. Before rows 59 and 61 it never reached the north +/// gate in 72,000 frames. The claims: +/// +/// - **no button is offered on an overworld frame inside a trainer's challenge** +/// (`wStatusFlags7` bit 3 set), and the run does reach such frames; +/// - **no tile of the forest is walled by a trainer's challenge**: (1, 18) never enters the pushed +/// ledger; +/// - **the fly goes through the north gate onto Route 2 and into Pewter City** inside the budget. +/// +/// ```sh +/// FLY_ROM=/path/to/pokemon-red.gb FLY_ACCEPT_ADAPTERS=pokered-unique8-v6 \ +/// FLY_ROW61_CHECKPOINT= \ +/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture forests_north_gate +/// ``` +#[test] +fn a_trainers_challenge_does_not_wall_the_road_to_the_forests_north_gate() { + use flybrain_gb::pokemon_red::macros::PokemonPalette; + use flybrain_gb::pokemon_red::macros::cartridge::{PushedLedger, Tile}; + use flybrain_gb::pokemon_red::state; + use flybrain_gb::{MacroPalette, MemoryReader, Started}; + const FOREST: u8 = 0x33; + const NORTH_GATE: u8 = 0x2f; + const ROUTE_2: u8 = 0x0d; + const PEWTER: u8 = 0x02; + let rom = skip_without_rom!(); + let Some(checkpoint) = row61_checkpoint() else { + eprintln!("skipped: no FLY_ROW61_CHECKPOINT"); + return; + }; + let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint); + assert_eq!(run.map(), 0x32, "the checkpoint is the forest's south gate"); + + let budget = 40_000u32; + let hold_frames = 48u32; + let mut palette = PokemonPalette::new(SEED); + let mut rng = 7u32; + let mut running = false; + let mut since_decision = hold_frames; + let mut ms = run.ms; + let mut arrivals: Vec<(u32, u8)> = Vec::new(); + let mut last = None; + let mut walled_at: Option = None; + // Overworld frames (the shared reading) inside a trainer's challenge, and those that dealt a pad. + let mut engaged = 0u32; + let mut engaged_dealt = 0u32; + for frame in 0..budget { + palette.clock(ms); + let observed = { + let ledger = AdapterLedger(&run.adapter); + palette.observe(&mut run.gb, &ledger) + }; + // `wStatusFlags7` bit 3, `BIT_TRAINER_BATTLE`, read here rather than through the seam. + if run.gb.read8(flybrain_gb::pokemon_red::symbols::ram::wStatusFlags7) & (1 << 3) != 0 + && flybrain_gb::pokemon_red::scene::detect(&mut run.gb) + == flybrain_gb::pokemon_red::macros::state::Scene::Overworld + { + engaged += 1; + if !observed.bindings.is_empty() { + engaged_dealt += 1; + } + } + let mut mask = 0u8; + { + let ledger = AdapterLedger(&run.adapter); + if running { + match palette.step(&mut run.gb, &ledger) { + Some(held) => mask = held, + None => running = false, + } + } else if since_decision >= hold_frames && !observed.bindings.is_empty() { + since_decision = 0; + rng ^= rng << 13; + rng ^= rng >> 17; + rng ^= rng << 5; + let binding = &observed.bindings[rng as usize % observed.bindings.len()]; + if let Started::Running(_) = palette.start(binding.slot, &mut run.gb, &ledger) { + running = true; + match palette.step(&mut run.gb, &ledger) { + Some(held) => mask = held, + None => running = false, + } + } + } + } + let _ = palette.take_finished(); + since_decision += 1; + run.gb.set_buttons(mask); + run.gb.run_frame().expect("a frame should complete"); + ms += MS_PER_FRAME; + run.adapter.sample(&mut run.gb, ms); + + if walled_at.is_none() && palette.fences().0.pushed(FOREST, Tile::new(1, 18)) { + walled_at = Some(frame); + } + if let Some(player) = state::player(&mut run.gb) + && last != Some(player.map) + { + last = Some(player.map); + arrivals.push((frame, player.map)); + } + } + let first = |map: u8| arrivals.iter().find(|(_, at)| *at == map).map(|(frame, _)| *frame); + let north_gate = first(NORTH_GATE); + let route_2_north = north_gate + .and_then(|gate| arrivals.iter().find(|(frame, map)| *frame > gate && *map == ROUTE_2)) + .map(|(frame, _)| *frame); + let pewter = first(PEWTER); + eprintln!( + "{:.1} brain minutes: challenge overworld frames {engaged}, a pad dealt on {engaged_dealt}; \ + (1, 18) walled at {walled_at:?}; north gate {north_gate:?}, Route 2 \ + after it {route_2_north:?}, Pewter City {pewter:?}; pushed {:?}; rank {}; arrivals {}", + (ms - run.ms) / 60_000.0, + palette.fences().0, + run.adapter.progress().rank, + arrivals.len(), + ); + assert!(engaged > 0, "the run never reached a trainer's challenge"); + assert_eq!(engaged_dealt, 0, "a pad was dealt inside a trainer's challenge"); + assert_eq!(walled_at, None, "a trainer's challenge walled the forest's corridor"); + assert!(north_gate.is_some(), "the fly never reached the forest's north gate: {arrivals:?}"); + assert!(route_2_north.is_some(), "nor Route 2 through it: {arrivals:?}"); + assert!(pewter.is_some(), "nor Pewter City: {arrivals:?}"); +}