//! Scene detection and the state accessors, against the real cartridge. //! //! Gated on `FLY_ROM`, like `tests/rom.rs`: the ROM never enters this repository and these tests //! skip cleanly without it. //! //! ```sh //! FLY_ROM="$HOME/fly-plays-pokemon/Pokemon Red (U) [S][BF].gb" \ //! cargo test --release --test rom_scene -- --nocapture //! ``` //! //! ## What only a cartridge can check //! //! The synthetic traces in `pokemon_red/scene/tests.rs` and `pokemon_red/state/tests.rs` check //! that the accessors read the bytes `docs/design/macros-wram.md` says they read. They cannot //! check that those are the bytes the game writes. Five things here can only be checked against a //! running cartridge, and each one is an assertion that would have caught a plausible mistake: //! //! 1. **The scene at each of the run's real states.** Title, the bedroom, the ground floor, Pallet //! Town, a dialogue in Oak's lab, and a battle. //! 2. **The screen-to-map mapping.** The walkable predicate reads the tile the player stands on at //! screen (8, 9) and two screen tiles per map tile. If the origin or the stride were wrong the //! predicate would still answer, plausibly, and wrongly — so it is compared against a survey //! that walks the map with real button presses on throwaway emulators, tile by tile, the method //! `docs/design/room-escape.md` section 3 used to find the six presses that leave Red's ground //! floor. //! 3. **That the collision list is reachable at all.** It lives in ROM, not WRAM; the claim that //! bank 0 is always mapped is a claim about the cartridge. //! 4. **The party after the starter**, which is the first party the game ever writes. //! 5. **A battle**, including whose turn it is, which no snapshot in the repository carries: the //! fixtures are a boot and two rooms of an empty house. Section "producing a battle" below //! records how this one is made. //! //! ## Producing the states //! //! There are no archived save states in the repository — `.gitignore` excludes `*.state` along //! with the ROM — so these tests produce their own the way `examples/room_escape.rs` does: boot //! the cartridge once, then drive it with a fixed-seed random walk and button pulses, stage by //! stage, each stage ending on a condition rather than a frame count. The emulator is //! deterministic and the walk is seeded, so the run is reproducible; the timeline is printed with //! `--nocapture` and the observed frame counts are in `docs/design/macros-wram.md`. //! //! A random walk rather than steering, for the same reason `tests/rom.rs` gives: steering needs a //! collision map to be any good, and using this crate's own walkable predicate to reach the state //! that tests the walkable predicate would be circular. The walk needs no map knowledge. //! //! ### Producing a battle //! //! The first battle a fresh cartridge can reach is the rival's, in Oak's lab, and the path to it //! is scripted: leaving Pallet Town to the north triggers Oak, who walks the player into the lab; //! taking a starter from a ball needs one A press on the right tile and one on the YES of a //! two-option menu; the rival then takes his and challenges the player where he stands. So the //! walk that reaches Pallet Town, with A pulses added, reaches a trainer battle with no knowledge //! of any of it. use flybrain_gb::adapter::{GameAdapter, MemoryReader}; use flybrain_gb::pokemon_red::macros::state::{BattleKind, BattleMenu, Scene, Walkable}; use flybrain_gb::pokemon_red::symbols::ram; use flybrain_gb::pokemon_red::{PokemonRedReward, SUPPORTED_ROM, scene, state}; use flybrain_gb::{DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, buttons}; use std::collections::{BTreeMap, BTreeSet}; /// `constants/map_constants.asm`. const REDS_HOUSE_1F: u8 = 0x25; const REDS_HOUSE_2F: u8 = 0x26; const PALLET_TOWN: u8 = 0x00; const OAKS_LAB: u8 = 0x28; /// One game frame at the Game Boy's real rate, for the adapter's brain clock. const MS_PER_FRAME: f64 = 1000.0 / 59.7275; fn rom() -> Option> { let path = std::env::var_os("FLY_ROM")?; match std::fs::read(&path) { Ok(bytes) => Some(bytes), Err(error) => panic!("FLY_ROM is set to {path:?} but could not be read: {error}"), } } fn emulator(rom: &[u8]) -> Emulator { Emulator::new(rom, DEFAULT_AUDIO_FREQUENCY, DEFAULT_AUDIO_FRAMES) .expect("binjgb should accept the cartridge") } macro_rules! skip_without_rom { () => { match rom() { Some(rom) => rom, None => { eprintln!("skipped: FLY_ROM is not set"); return; } } }; } /// The cartridge, the adapter and the brain clock, driven one frame at a time. struct Run { emulator: Emulator, adapter: PokemonRedReward, ms: f64, frame: u32, } impl Run { fn new(rom: &[u8]) -> Self { let emulator = emulator(rom); assert_eq!( emulator.rom_sha256(), SUPPORTED_ROM, "FLY_ROM is not the cartridge the adapter is pinned to" ); Self { emulator, adapter: PokemonRedReward::new(), ms: 0.0, frame: 0 } } /// One frame with these buttons held, then one adapter sample — the sim loop's order. fn step(&mut self, mask: u8) { self.emulator.set_buttons(mask); self.emulator.run_frame().expect("a frame should complete"); self.ms += MS_PER_FRAME; self.frame += 1; self.adapter.sample(&mut self.emulator, self.ms); } fn scene(&mut self) -> Scene { scene::detect(&mut self.emulator) } fn map(&mut self) -> u8 { self.emulator.read_wram(ram::wCurMap) } fn tile(&mut self) -> (u8, u8) { (self.emulator.read_wram(ram::wXCoord), self.emulator.read_wram(ram::wYCoord)) } fn party_count(&mut self) -> u8 { self.emulator.read_wram(ram::wPartyCount) } /// `EVENT_FOLLOWED_OAK_INTO_LAB`, bit 0 of `wEventFlags`: whether Oak has walked the player /// into his lab, which is what makes the three balls takeable. fn followed_oak(&mut self) -> bool { self.emulator.read_wram(ram::wEventFlags) & 1 != 0 } fn why(&mut self) -> String { scene::why_unknown(&mut self.emulator) } /// Mash Start and A through the intro and the naming screens, then B until the adapter reports /// a settled, dialogue-free overworld sample. `tests/rom.rs` explains the pattern: a held /// button is ignored, and the intro's A-mashing leaves a text box open that only B dismisses. fn boot_to_bedroom(&mut self) { for frame in 0..8_000u32 { let mask = match frame % 32 { 0..=7 => buttons::START, 16..=23 => buttons::A, _ => buttons::NONE, }; self.step(mask); if frame > 3_000 && self.adapter.mode() == "OVERWORLD" { break; } } for frame in 0..12_000u32 { self.step(if frame % 24 < 8 { buttons::B } else { buttons::NONE }); if self.adapter.safe_for_snapshot() { break; } } let settled = self.adapter.safe_for_snapshot(); let mode = self.adapter.mode().to_string(); assert!(settled, "the intro never settled (mode {mode}, {})", self.why()); assert_eq!(self.adapter.map_id(), Some(u32::from(REDS_HOUSE_2F))); } /// Drive the cartridge until `done` says the stage is over, and return the frame it ended on. /// /// The pattern per 48-frame cycle is a direction, then B, then the stage's own button: B /// closes whatever a stray A opened, which is the difference between a walk that explores and /// one that stands in front of a bookshelf reading it four hundred thousand times (measured: /// an A-only walk never left the tile it entered Oak's lab on). /// /// `bias` aims the walk. With `None` the direction is uniform, which is what `tests/rom.rs` /// uses and needs no map knowledge; with `Some(direction)` three presses in four are that /// direction and the fourth is random, which is how a stage that has to leave a town by a /// particular edge gets there in a test's worth of frames. The bias is the test's, not the /// fly's: it is how the state under test is *produced*, never part of what is asserted. fn drive_until( &mut self, what: &str, budget: u32, seed: u64, press: u8, mut bias: impl FnMut(&mut Run) -> Option, mut done: impl FnMut(&mut Run) -> bool, ) -> u32 { let start = self.frame; let mut state = seed; let mut visited = BTreeSet::new(); for frame in 0..budget { state = state .wrapping_mul(6_364_136_223_846_793_005) .wrapping_add(1_442_695_040_888_963_407); let random = [buttons::UP, buttons::DOWN, buttons::LEFT, buttons::RIGHT] [((state >> 33) % 4) as usize]; let step = match bias(self) { // Half biased, half random: the random half is what gets the walk around the // furniture the bias walks it into. Some(direction) if (state >> 41).is_multiple_of(2) => direction, _ => random, }; let mask = match frame % 48 { 0..=7 | 24..=31 => step, 12..=15 => buttons::B, 36..=39 => press, _ => buttons::NONE, }; self.step(mask); let tile = self.tile(); visited.insert((self.map(), tile.0, tile.1)); if done(self) { eprintln!( "{what}: reached at frame {} ({} frames into the stage, {} tiles visited)", self.frame, self.frame - start, visited.len() ); return self.frame; } } panic!( "{what}: not reached in {budget} frames (map {:#04x}, tile {:?}, party {}, \ {} tiles visited, {})", self.map(), self.tile(), self.party_count(), visited.len(), self.why() ); } } /// The uniform random walk, which needs no knowledge of any map. fn no_bias(_: &mut Run) -> Option { None } /// Aim the walk at the starter. /// /// This is the test's knowledge of the game, not the fly's: it decides which states get /// *produced*, and nothing it returns is ever asserted. Three facts, each from the disassembly: /// /// - `PalletTownDefaultScript` triggers on `wYCoord == 1`, so reaching the top row of Pallet Town /// anywhere along it is enough; Oak then walks the player into the lab himself. /// - the two house doors in Pallet Town are at x 5 and x 13 (`warp_event 5, 5` and /// `warp_event 13, 5`) and a door warps on the step onto it, so an UP bias from the spawn walks /// straight back indoors. Step off those columns first. /// - the three balls are at the top of Oak's lab, and they are only takeable once /// `EVENT_FOLLOWED_OAK_INTO_LAB` is set, so the lab is somewhere to leave before that and /// somewhere to walk north in after it. fn toward_the_starter(run: &mut Run) -> Option { let map = run.map(); match map { PALLET_TOWN => { Some(if matches!(run.tile().0, 5 | 13) { buttons::LEFT } else { buttons::UP }) } OAKS_LAB if run.followed_oak() => Some(buttons::UP), _ => Some(buttons::DOWN), } } /// Walk a map with real button presses on throwaway emulators. /// /// `examples/room_escape.rs`'s `survey`, with the presses that left the map recorded per tile. /// Returns every tile reachable from the starting state and, for each tile, the presses that leave /// the map. type Survey = ( BTreeSet<(u8, u8)>, BTreeMap<(u8, u8), Vec<&'static str>>, BTreeMap<(u8, u8), Vec>, ); fn survey(rom: &[u8], state: &[u8]) -> Survey { let read = |probe: &mut Emulator| { ( probe.read8(ram::wCurMap), probe.read8(ram::wXCoord), probe.read8(ram::wYCoord), ) }; // Hold the direction until the coordinates change, then release and let the machine settle // before the state is kept. Two numbers here were measured rather than guessed: // // - **120 frames**, not `examples/room_escape.rs`'s 48. A press the player is not already // facing turns it first and steps second, and the pair took 53 frames from the ground // floor's staircase: a 48-frame window reads that as a wall. // - **the release.** A state exported while a button was held does not respond to that // button, or to any other, being held again from the frame after the import — measured on // this cartridge, all four directions, 64 frames each, no movement. Releasing and running // twenty frames before the export fixes it, so every state this survey keeps is settled. let step = |probe: &mut Emulator, mask: u8| { let before = read(probe); probe.set_buttons(mask); let mut moved = false; for _ in 0..120 { probe.run_frame().expect("a frame should complete"); if read(probe) != before { moved = true; break; } } probe.set_buttons(buttons::NONE); for _ in 0..20 { probe.run_frame().expect("a frame should complete"); } (read(probe), moved) }; let restore = |state: &[u8]| { let mut probe = emulator(rom); probe.import_state(state).expect("a surveyed state should import"); probe }; let mut first = restore(state); let (start_map, x, y) = read(&mut first); let mut reachable = BTreeSet::new(); let mut exits: BTreeMap<(u8, u8), Vec<&'static str>> = BTreeMap::new(); let mut states: BTreeMap<(u8, u8), Vec> = BTreeMap::new(); reachable.insert((x, y)); states.insert((x, y), state.to_vec()); let mut queue = vec![(x, y)]; while let Some(tile) = queue.pop() { for (name, mask) in [ ("up", buttons::UP), ("down", buttons::DOWN), ("left", buttons::LEFT), ("right", buttons::RIGHT), ] { let mut probe = restore(&states[&tile]); let ((map, x, y), moved) = step(&mut probe, mask); if !moved { continue; } if map != start_map { exits.entry(tile).or_default().push(name); continue; } if reachable.insert((x, y)) { states.insert((x, y), probe.export_state().expect("state export")); queue.push((x, y)); } } } (reachable, exits, states) } #[test] fn the_title_screen_is_the_title_scene_and_nothing_else() { let rom = skip_without_rom!(); let mut run = Run::new(&rom); // A cold boot's WRAM is pseudorandom (binjgb seeds it), so the first frames can read a garbage // game-timer bit; `tests/rom.rs` records the same caveat. Let the boot code clear WRAM first. for _ in 0..600 { run.step(buttons::NONE); } let mut scenes = BTreeSet::new(); for _ in 0..3_000 { run.step(buttons::NONE); scenes.insert(format!("{:?}", run.scene())); } eprintln!("title: scenes over 3,000 idle frames after the boot logo = {scenes:?}"); assert_eq!( scenes.len(), 1, "the title screen should be one scene and one scene only" ); assert_eq!(run.scene(), Scene::Title); // And the adapter agrees, which is what keeps the palette's Title and the page's BOOT one // thing rather than two. assert_eq!(run.adapter.mode(), "BOOT"); // Nothing else is readable there: no player, no party, no walkable tile. assert_eq!(state::player(&mut run.emulator), None); assert!(state::party(&mut run.emulator).mons.is_empty()); assert_eq!(state::walkable(&mut run.emulator, 3, 6), Walkable::Unknown); } #[test] fn the_bedroom_the_ground_floor_and_pallet_town_are_the_overworld() { let rom = skip_without_rom!(); let mut run = Run::new(&rom); run.boot_to_bedroom(); // 1. The bedroom. assert_eq!(run.scene(), Scene::Overworld, "{}", run.why()); let player = state::player(&mut run.emulator).expect("a loaded map"); assert_eq!(player.map, REDS_HOUSE_2F); eprintln!("bedroom: player at ({}, {}) facing {:?}", player.x, player.y, player.facing); assert_eq!( state::map_size(&mut run.emulator).map(|size| (size.width, size.height)), Some((8, 8)), "Red's bedroom is 4 by 4 blocks" ); // The party is empty before the starter, and that is a fact about the cartridge rather than // about the accessor: `wPartyCount` is zero and the six party structs are untouched. assert!(state::party(&mut run.emulator).mons.is_empty()); assert_eq!(state::money(&mut run.emulator), 3_000, "the starting wallet"); assert!(state::bag(&mut run.emulator).is_empty()); assert!(state::battle(&mut run.emulator).is_none()); assert!(state::start_menu(&mut run.emulator).is_none()); assert!(state::shop(&mut run.emulator).is_none()); assert!(state::pc(&mut run.emulator).is_none()); // The bedroom's one warp is the staircase, `warp_event 7, 1, REDS_HOUSE_1F, 3`. let warps = state::warps(&mut run.emulator); eprintln!("bedroom: warps {warps:?}"); assert_eq!(warps.len(), 1); assert_eq!((warps[0].x, warps[0].y), (7, 1)); assert_eq!(warps[0].destination_map, REDS_HOUSE_1F); // `warp_event`'s fourth argument is one-based and the byte is not: `MACRO warp_event` emits // `db \2, \1, \4 - 1, \3`, so the declared warp 3 is stored as 2. assert_eq!(warps[0].destination_warp, 2, "the declared warp 3, stored zero-based"); assert!( !state::connections(&mut run.emulator).any(), "an indoor map has no connected edges" ); // 2. The ground floor. run.drive_until("downstairs", 40_000, 0x5eed_1234_5678_9abc, buttons::B, no_bias, |run| { run.map() == REDS_HOUSE_1F && run.adapter.safe_for_snapshot() }); assert_eq!(run.scene(), Scene::Overworld, "{}", run.why()); assert_eq!(state::player(&mut run.emulator).map(|player| player.map), Some(REDS_HOUSE_1F)); // 3. Pallet Town. run.drive_until("pallet town", 80_000, 0x1234_5678_9abc_def0, buttons::B, no_bias, |run| { run.map() == PALLET_TOWN && run.adapter.safe_for_snapshot() }); assert_eq!(run.scene(), Scene::Overworld, "{}", run.why()); let player = state::player(&mut run.emulator).expect("a loaded map"); assert_eq!(player.map, PALLET_TOWN); assert_eq!( state::map_size(&mut run.emulator).map(|size| (size.width, size.height)), Some((20, 18)), "Pallet Town is 10 by 9 blocks" ); let connections = state::connections(&mut run.emulator); eprintln!("pallet town: connections {connections:?}, warps {}", state::warps(&mut run.emulator).len()); assert!(connections.north, "Route 1 is north of Pallet Town"); assert!(connections.south, "Route 21 is south of it"); assert!(!connections.east && !connections.west); // Outdoors the map is wider than the screen's window, so the far side of the town is not // answerable and must say so rather than guess. let far = state::walkable(&mut run.emulator, 19, 17); eprintln!("pallet town: walkable(19, 17) = {far:?} from ({}, {})", player.x, player.y); if player.x.abs_diff(19) > 5 || player.y.abs_diff(17) > 4 { assert_eq!(far, Walkable::Unknown); } } #[test] fn the_walkable_predicate_agrees_with_a_survey_of_the_ground_floor() { let rom = skip_without_rom!(); let mut run = Run::new(&rom); run.boot_to_bedroom(); run.drive_until("downstairs", 40_000, 0x5eed_1234_5678_9abc, buttons::B, no_bias, |run| { run.map() == REDS_HOUSE_1F && run.adapter.safe_for_snapshot() }); // Release and settle before exporting: a state exported mid-press does not respond to a held // button afterwards (measured; see `survey`). for _ in 0..20 { run.step(buttons::NONE); } let ground_floor = run.emulator.export_state().expect("state export"); // The survey: real presses, throwaway emulators, no map knowledge. let (reachable, exits, states) = survey(&rom, &ground_floor); eprintln!( "survey: map {REDS_HOUSE_1F:#04x} has {} reachable tiles and {} tiles with an exit", reachable.len(), exits.len() ); assert_eq!( reachable.len(), 48, "`docs/design/room-escape.md` section 3 counted 48 reachable tiles" ); // The six presses that leave the map, exactly as section 3 tabulates them. let mut found: Vec<((u8, u8), &'static str)> = exits .iter() .flat_map(|(tile, presses)| presses.iter().map(move |press| (*tile, *press))) .collect(); found.sort(); let mut expected: Vec<((u8, u8), &'static str)> = vec![ ((6, 1), "right"), ((7, 2), "up"), ((2, 6), "down"), ((3, 6), "down"), ((2, 7), "down"), ((3, 7), "down"), ]; expected.sort(); eprintln!("survey: exits {found:?}"); assert_eq!(found, expected, "the six presses that leave Red's ground floor"); // Now the predicate, against the survey, from every one of the 48 tiles. This is the // assertion that catches a wrong screen origin or a wrong stride: those still answer, and // still answer plausibly, because they read a real tile id from the wrong place. // // One rule of the emulator matters here and is worth recording: `wTileMap` is only the // current view on a *running* machine. Read straight after `import_state`, with no frame in // between, it holds the view from wherever the state was taken, and the predicate answers // about the wrong tiles — measured, four tiles of this map disagreed exactly that way. So // every restored state is given twenty idle frames before it is read, which is also what the // sim loop does by construction: the adapter samples after a completed frame. let mut probe = emulator(&rom); let mut checked = 0usize; let mut blocked_by_an_npc = Vec::new(); let mut mismatches = Vec::new(); let mut size = None; for (tile, state) in &states { probe.import_state(state).expect("a surveyed state should import"); for _ in 0..20 { probe.set_buttons(buttons::NONE); probe.run_frame().expect("a frame should complete"); } let player = state::player(&mut probe).expect("a loaded map"); assert_eq!((player.x, player.y), *tile, "the survey's state for {tile:?}"); let map = state::map_size(&mut probe).expect("a loaded map"); size = Some(map); let npcs = state::npcs(&mut probe); for (press, (dx, dy)) in [("up", (0, -1)), ("down", (0, 1)), ("left", (-1, 0)), ("right", (1, 0))] { let (nx, ny) = (i32::from(tile.0) + dx, i32::from(tile.1) + dy); if nx < 0 || ny < 0 { // A tile coordinate cannot be negative, so there is nothing to ask about. continue; } if nx >= i32::from(map.width) || ny >= i32::from(map.height) { // Off the map. The two doormats leave it downwards from the bottom row, and they // do it by warping rather than by stepping onto a tile, so there is no walkable // tile out there to find. assert_eq!( state::walkable(&mut probe, nx as u8, ny as u8), Walkable::No, "off the map from {tile:?} pressing {press}" ); continue; } let (nx, ny) = (nx as u8, ny as u8); let moved = reachable.contains(&(nx, ny)) || exits.get(tile).is_some_and(|presses| presses.contains(&press)); let answer = state::walkable(&mut probe, nx, ny); checked += 1; match (answer, moved) { (Walkable::Yes, true) | (Walkable::No, false) => {} // A tile whose id is passable but which an NPC is standing on: the predicate is // about tiles and says so, and `npcs` is the accessor that covers the difference. (Walkable::Yes, false) if npcs.iter().any(|npc| (npc.x, npc.y) == (nx, ny)) => { blocked_by_an_npc.push((*tile, press, (nx, ny))); } _ => mismatches.push((*tile, press, (nx, ny), answer, moved)), } } } eprintln!( "survey: the predicate agreed with the survey on {checked} presses from {} tiles; \ {} blocked by an NPC standing on a passable tile", states.len(), blocked_by_an_npc.len() ); if !blocked_by_an_npc.is_empty() { eprintln!("survey: NPC-blocked {blocked_by_an_npc:?}"); } assert!(mismatches.is_empty(), "the predicate disagreed with the survey: {mismatches:?}"); assert!(checked >= 150, "only {checked} presses were compared"); let size = size.expect("at least one surveyed tile"); assert_eq!((size.width, size.height), (8, 8), "Red's ground floor is 4 by 4 blocks"); // And the tiles the six presses act on, named: the staircase, which both of the presses that // leave upstairs act on, and the two doormats, which the two presses from the row above act // on. `GO EXIT` needs exactly these to be walkable. probe.import_state(&states[&(6, 1)]).expect("state import"); for _ in 0..20 { probe.run_frame().expect("a frame should complete"); } assert_eq!(state::walkable(&mut probe, 7, 1), Walkable::Yes, "the staircase at (7, 1)"); let warps = state::warps(&mut probe); eprintln!("survey: warps {warps:?}"); assert_eq!(warps.len(), 3, "two doormats and the staircase"); assert!(warps.iter().any(|warp| (warp.x, warp.y) == (7, 1) && warp.destination_map == REDS_HOUSE_2F)); // `warp_event 2, 7, LAST_MAP, 1` and `warp_event 3, 7, LAST_MAP, 1`: the doormats go back to // whichever map the player came from, which the cartridge writes as $ff. assert_eq!( warps.iter().filter(|warp| warp.y == 7 && warp.destination_map == 0xff).count(), 2, "the two doormats" ); // `warp_event`'s fourth argument is one-based and the byte is not: the macro emits `\4 - 1`. assert!( warps.iter().all(|warp| warp.destination_warp == 0), "every warp on this map declares destination warp 1, stored as 0: {warps:?}" ); probe.import_state(&states[&(3, 6)]).expect("state import"); for _ in 0..20 { probe.run_frame().expect("a frame should complete"); } for mat in [(2u8, 7u8), (3, 7)] { assert_eq!( state::walkable(&mut probe, mat.0, mat.1), Walkable::Yes, "the doormat at {mat:?}" ); } assert!( !state::connections(&mut probe).any(), "an indoor map has no connected edges: the way out is a warp" ); } #[test] fn oaks_lab_produces_a_dialog_a_starter_and_a_battle() { let rom = skip_without_rom!(); let mut run = Run::new(&rom); run.boot_to_bedroom(); run.drive_until("pallet town", 120_000, 0x5eed_1234_5678_9abc, buttons::B, no_bias, |run| { run.map() == PALLET_TOWN && run.adapter.safe_for_snapshot() }); // One stage carries the rest of the run: Oak's script, the lab, the dialogue in it and the // starter. Oak triggers on `wYCoord == 1` — the top row of Pallet Town, anywhere along it — // and then walks the player into the lab himself, where the three balls are. The dialogue is // recorded as it goes past rather than aimed at, because there is no state in which a lab // without a dialogue in it is interesting: Oak talks the whole way through. let mut lab_dialog = None; let mut lab_dialog_state = None; let mut counted = None; run.drive_until("a starter", 900_000, 0x1357_9bdf_2468_ace0, buttons::A, toward_the_starter, |run| { if run.party_count() > 0 && counted.is_none() { counted = Some(run.frame); } if run.map() == OAKS_LAB && run.scene() == Scene::Dialog && lab_dialog.is_none() { lab_dialog = Some(run.frame); let text = state::text_box(&mut run.emulator); lab_dialog_state = Some(( text.open, text.waiting, state::player(&mut run.emulator).is_some(), state::walkable(&mut run.emulator, 4, 4), )); } // `wPartyCount` leads the party struct: `AddPartyMon` writes the count first and fills // the 44 bytes over the frames after it, so the count alone is not a Pokémon yet. This // is the gotcha `docs/design/macros-wram.md` records for the party accessor, measured // here as the gap between `counted` and this condition. state::party(&mut run.emulator) .mons .first() .is_some_and(|mon| mon.species != 0 && mon.level != 0 && mon.max_hp != 0) }); let dialog = lab_dialog.expect("a dialogue in Oak's lab"); let (open, waiting, map_loaded, walkable_under_the_box) = lab_dialog_state.expect("the dialogue's readings"); eprintln!("dialog: first seen in Oak's lab at frame {dialog}"); assert!(open && waiting, "a dialog is an open box that waits: open {open} waiting {waiting}"); // A dialog is not the overworld, however much of the map is still underneath it. assert!(map_loaded, "the map is still loaded during a dialogue"); assert_eq!( walkable_under_the_box, Walkable::Unknown, "the screen buffer holds the text box, not the map" ); let counted = counted.expect("the party count changed"); let party = state::party(&mut run.emulator); eprintln!( "starter: wPartyCount became 1 at frame {counted}, the species was written {} frames \ later; party {party:?}", run.frame - counted ); assert!( run.frame > counted, "the count and the species landing on the same frame would make the note in \ macros-wram.md wrong" ); assert_eq!(party.mons.len(), 1); let starter = party.mons[0]; // Species ids are pokered's *internal* indices, not Pokédex numbers: BULBASAUR is $99, // CHARMANDER $b0 and SQUIRTLE $b1 (`constants/pokemon_constants.asm`), while the adapter's // Pokédex bitset is by Pokédex number. The accessor passes the byte through and // `docs/design/macros-wram.md` says which numbering it is; asserting 1, 4 or 7 here is the // mistake this assertion exists to prevent. assert!( [0x99u8, 0xb0, 0xb1].contains(&starter.species), "the starter is Bulbasaur ($99), Charmander ($b0) or Squirtle ($b1), got {:#04x}", starter.species ); assert_eq!(starter.level, 5, "every starter is level 5"); assert_eq!(starter.hp, starter.max_hp, "a gift Pokémon arrives at full HP"); assert!((18..=21).contains(&starter.max_hp), "max HP {}", starter.max_hp); assert!(!starter.fainted()); assert!( (starter.hp_fraction() - 1.0).abs() < 1e-12, "fraction {}", starter.hp_fraction() ); // Level-1 learnsets: Bulbasaur and Squirtle know TACKLE (33), Charmander SCRATCH (10), and // all three know a second move — GROWL (45) or TAIL WHIP (39). Both damaging moves have 35 // PP, which is what makes the packed PP byte readable here. let first = starter.moves[0].expect("a starter knows at least one move"); assert!([33u8, 10].contains(&first.id), "first move {}", first.id); assert_eq!(first.pp, 35, "TACKLE and SCRATCH both have 35 PP"); assert_eq!(first.pp_up, 0); let second = starter.moves[1].expect("a starter knows two moves"); assert!([45u8, 39].contains(&second.id), "second move {}", second.id); assert!(starter.moves[2].is_none() && starter.moves[3].is_none()); // The battle: the rival takes his starter and challenges the player where he stands. run.drive_until("a battle", 400_000, 0x2468_ace0_1357_9bdf, buttons::A, no_bias, |run| { matches!(run.scene(), Scene::Battle { .. }) }); let scene = run.scene(); assert!(matches!(scene, Scene::Battle { .. }), "{scene:?}"); eprintln!("battle: first frame {scene:?}, {:?}", state::battle(&mut run.emulator)); // On the frame a battle starts the combatants are not written yet — the reward adapter's own // comment says the same about `wEnemyMonSpecies` — so the scene is a battle before there is // anything in it. That is the right order for the detector and it is why the palette's // `own_turn` is a separate question from "is this a battle". assert_eq!(run.adapter.mode(), "BATTLE"); assert_eq!( state::walkable(&mut run.emulator, 4, 4), Walkable::Unknown, "a battle is never the overworld, whatever is on screen" ); // Wait for the fly's turn: the top-level FIGHT / PKMN / ITEM / RUN menu, which arrives after // the opening text. B advances text and does nothing to that menu, so it cannot overshoot // into the move list the way A would. let mut own_turn = None; for frame in 0..8_000u32 { run.step(if frame % 24 < 8 { buttons::B } else { buttons::NONE }); if let Scene::Battle { own_turn: true, .. } = run.scene() { own_turn = Some(run.frame); break; } } let own_turn = own_turn.unwrap_or_else(|| { panic!("the battle menu never opened ({}, {:?})", run.why(), run.scene()) }); let battle = state::battle(&mut run.emulator).expect("a battle"); eprintln!("battle: the menu opened at frame {own_turn}, {battle:?}"); assert!(battle.own_turn && !battle.forced_switch); assert_eq!(battle.kind, BattleKind::Trainer, "the rival is a trainer, not a wild Pokémon"); assert_eq!( battle.menu, BattleMenu::Main { cursor: 0 }, "a fresh battle menu starts on FIGHT" ); let enemy = battle.enemy.expect("an opposing Pokémon"); assert!( [0x99u8, 0xb0, 0xb1].contains(&enemy.species), "the rival's starter, got {:#04x}", enemy.species ); assert_ne!(enemy.species, starter.species, "the rival takes the other one"); assert_eq!(enemy.level, 5); assert_eq!(enemy.hp, enemy.max_hp, "a fresh battle"); let own = battle.own.expect("the Pokémon that is out"); assert_eq!(own.species, starter.species); assert_eq!(own.max_hp, starter.max_hp, "the battler is a copy of the party entry"); assert_eq!(own.moves[0].map(|first| first.id), starter.moves[0].map(|first| first.id)); // In a battle the party reports which slot is out. assert_eq!(state::party(&mut run.emulator).active, Some(0)); // The cursor is real: RIGHT moves it to the other column, which is ITEM. for frame in 0..72u32 { run.step(if frame % 24 < 8 { buttons::RIGHT } else { buttons::NONE }); } let battle = state::battle(&mut run.emulator).expect("a battle"); eprintln!("battle: after RIGHT, menu {:?}", battle.menu); assert_eq!(battle.menu, BattleMenu::Main { cursor: 2 }, "RIGHT from FIGHT is ITEM"); assert!(battle.own_turn, "the menu is still the fly's turn"); // And DOWN from ITEM is RUN. for frame in 0..72u32 { run.step(if frame % 24 < 8 { buttons::DOWN } else { buttons::NONE }); } let battle = state::battle(&mut run.emulator).expect("a battle"); eprintln!("battle: after DOWN, menu {:?}", battle.menu); assert_eq!(battle.menu, BattleMenu::Main { cursor: 3 }, "DOWN from ITEM is RUN"); // The adapter's own mode agrees, so the palette and the page cannot disagree about a battle. assert_eq!(run.adapter.mode(), "BATTLE"); }