state, tests: the challenge covers the "!" bubble too, the doc block back above its impl, and the ROM test asserts no pad inside a challenge

This commit is contained in:
acamilo 2026-09-23 23:47:36 +00:00
parent d6597b3e98
commit cff15804fb
4 changed files with 53 additions and 30 deletions

View file

@ -618,16 +618,16 @@ mod tests {
#[test] #[test]
fn the_frames_between_a_trainers_text_and_its_battle_deal_no_pad_and_record_no_ground() { 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 challenge text closes onto five // Row 61, Viridian Forest. A trainer who saw the fly: its "!" bubble and the five frames
// frames that read as an ordinary overworld -- no box, no script bit, `wCurOpponent` // after its challenge text read as an ordinary overworld -- no box, no script bit,
// still zero -- before the battle is decided. The pad was dealt there and the push-back // `wJoyIgnore` and `wCurOpponent` zero. The pad was dealt there and the push-back the
// the fly's walk had earned when the trainer took the joypad was written there, walling // fly's walk had earned when the trainer took the joypad was written there, walling the
// the one free tile of the corridor to the north gate for the session. // one free tile of the corridor to the north gate for the session.
let mut wram = Wram::overworld(); let mut wram = Wram::overworld();
wram.set(crate::pokemon_red::symbols::ram::wStatusFlags7, 1 << 3); wram.set(crate::pokemon_red::symbols::ram::wStatusFlags7, 1 << 3);
let mut palette = PokemonPalette::new(7); let mut palette = PokemonPalette::new(7);
let engaged = palette.observe(&mut wram, &NoLedger); let engaged = palette.observe(&mut wram, &NoLedger);
assert_eq!(engaged.scene, SceneId::Unknown, "the cartridge's, between text and battle"); assert_eq!(engaged.scene, SceneId::Unknown, "the cartridge's, inside the challenge");
assert!(engaged.bindings.is_empty(), "nothing to press: {:?}", engaged.bindings); assert!(engaged.bindings.is_empty(), "nothing to press: {:?}", engaged.bindings);
assert_eq!(palette.stood(), 0, "and no ground recorded from it"); assert_eq!(palette.stood(), 0, "and no ground recorded from it");

View file

@ -1921,26 +1921,29 @@ impl GameState for PokeState<'_> {
} }
} }
/// The cartridge tables on their defaults, and the exploration ledger wired through.
///
/// Whether a trainer who saw the player is between its "!" and the end of its battle (row 61). /// 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 /// `DisplayEnemyTrainerTextAndStartBattle` (`home/trainers.asm`) clears `wJoyIgnore` before the
/// challenge text and calls `StartTrainerBattle`, which writes `wCurOpponent`, only after that /// text and calls `StartTrainerBattle`, which writes `wCurOpponent`, only after the text's
/// text's close-down has redrawn the map. Measured in Viridian Forest: five frames with the box /// close-down has redrawn the map. About sixty-six frames per engagement, every bit
/// gone, every bit [`controllable`] reads clear and `wCurOpponent` still zero, and then the /// [`controllable`] reads clear. The push-back a walk earned when the trainer took the joypad (row
/// battle. The macro seam read them as an overworld the fly owned, so the push-back a walk earned /// 58's held entry) was written on the first frame after the text: the one free tile beside the
/// when the trainer took the joypad (row 58's held entry) was written on the first of them: the /// trainer, in the only corridor to the forest's north gate, walled for the session.
/// 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 macros' reading only.** [`controllable`] and [`super::scene::detect`] are shared with
/// the reward adapter (the talk payout's "ready" test, the feed's scene), which this row does not /// the reward adapter (the talk payout's "ready" test) and do not change; [`PokeState`]'s own
/// change; [`PokeState`]'s own `scene` and `scripted` read this beside them. /// `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 { pub fn trainer_engaged(memory: &mut dyn MemoryReader) -> bool {
read(memory, ram::wStatusFlags7) & poke::TRAINER_BATTLE_STATUS7 != 0 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 /// `pokemon_red/macros/cartridge.rs` defaults every [`MacroState`] method and every default
/// *narrows* what the palette offers, so the executor runs over live WRAM with no overrides at /// *narrows* what the palette offers, so the executor runs over live WRAM with no overrides at
/// all and each one turned on later widens it without changing a signature. Two are still on /// all and each one turned on later widens it without changing a signature. Two are still on

View file

@ -1226,9 +1226,9 @@ fn a_refusal_that_cannot_be_read_is_not_reported() {
#[test] #[test]
fn a_trainers_challenge_is_the_cartridges_until_its_battle_is_over() { fn a_trainers_challenge_is_the_cartridges_until_its_battle_is_over() {
use crate::pokemon_red::macros::state::GameState; use crate::pokemon_red::macros::state::GameState;
// Row 61. A trainer who saw the fly: its text closes onto five frames with every bit // Row 61. A trainer who saw the fly: its "!" bubble (before `wJoyIgnore` is set) and the five
// `controllable` reads clear and `wCurOpponent` still zero, then the battle. The macros read // frames after its text (before `wCurOpponent` is) have every bit `controllable` reads clear.
// them as the cartridge's; the shared readings the reward adapter and the feed use do not // The macros read them as the cartridge's; the shared readings the reward adapter uses do not
// move. // move.
let mut wram = Wram::overworld(); let mut wram = Wram::overworld();
assert_eq!(PokeState::new(&mut wram).scene(), Scene::Overworld); assert_eq!(PokeState::new(&mut wram).scene(), Scene::Overworld);
@ -1236,10 +1236,10 @@ fn a_trainers_challenge_is_the_cartridges_until_its_battle_is_over() {
wram.set(ram::wStatusFlags7, poke::TRAINER_BATTLE_STATUS7); wram.set(ram::wStatusFlags7, poke::TRAINER_BATTLE_STATUS7);
assert!(trainer_engaged(&mut wram)); assert!(trainer_engaged(&mut wram));
assert_eq!(PokeState::new(&mut wram).scene(), Scene::Unknown, "between the text and the battle"); 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!(PokeState::new(&mut wram).scripted(), "and the fly is not its own master");
assert!(controllable(&mut wram), "the adapter's gate is unchanged"); assert!(controllable(&mut wram), "the adapter's gate is unchanged");
assert_eq!(crate::pokemon_red::scene::detect(&mut wram), Scene::Overworld, "and the feed's scene"); 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. // The challenge's own text is still a conversation to advance.
wram.dialogue_box(); wram.dialogue_box();

View file

@ -3676,21 +3676,25 @@ fn row61_checkpoint() -> Option<flysim::store::Checkpoint> {
/// `GO WARP` back from the forest, `GO OBJECTIVE blocked` in the forest, no reward /// `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 /// (`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 /// corridor at x = 1-2 with a Bug Catcher standing on (2, 18) facing west. A walk up the corridor
/// steps onto (1, 18), the trainer takes the joypad, and his text closes onto five frames the seam /// steps onto (1, 18) and the trainer takes the joypad. His "!" bubble (about sixty frames, before
/// read as the fly's own overworld: the push-back the walk had earned was written there, (1, 18) /// `wJoyIgnore` is set) and the five frames after his text (before `wCurOpponent` is) read as the
/// was walled for the session, and every later walk to the north gate had no road. /// 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 /// 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. On the base it never reaches the north gate in /// seeded 7 -- a harness choice, not the fly's. Before rows 59 and 61 it never reached the north
/// 72,000 frames. The claims: /// 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 /// - **no tile of the forest is walled by a trainer's challenge**: (1, 18) never enters the pushed
/// ledger; /// ledger;
/// - **the fly goes through the north gate onto Route 2 and into Pewter City** inside the budget. /// - **the fly goes through the north gate onto Route 2 and into Pewter City** inside the budget.
/// ///
/// ```sh /// ```sh
/// FLY_ROM=/path/to/pokemon-red.gb FLY_ACCEPT_ADAPTERS=pokered-unique8-v6 \ /// FLY_ROM=/path/to/pokemon-red.gb FLY_ACCEPT_ADAPTERS=pokered-unique8-v6 \
/// FLY_ROW61_CHECKPOINT=.local/checkpoints/release-rank9-row61.checkpoint \ /// FLY_ROW61_CHECKPOINT=<the rank-9 checkpoint pulled during the ring, under .local/checkpoints> \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture forests_north_gate /// cargo test --release -p flysim --test rom_macros_mode -- --nocapture forests_north_gate
/// ``` /// ```
#[test] #[test]
@ -3698,7 +3702,7 @@ 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::PokemonPalette;
use flybrain_gb::pokemon_red::macros::cartridge::{PushedLedger, Tile}; use flybrain_gb::pokemon_red::macros::cartridge::{PushedLedger, Tile};
use flybrain_gb::pokemon_red::state; use flybrain_gb::pokemon_red::state;
use flybrain_gb::{MacroPalette, Started}; use flybrain_gb::{MacroPalette, MemoryReader, Started};
const FOREST: u8 = 0x33; const FOREST: u8 = 0x33;
const NORTH_GATE: u8 = 0x2f; const NORTH_GATE: u8 = 0x2f;
const ROUTE_2: u8 = 0x0d; const ROUTE_2: u8 = 0x0d;
@ -3721,12 +3725,25 @@ fn a_trainers_challenge_does_not_wall_the_road_to_the_forests_north_gate() {
let mut arrivals: Vec<(u32, u8)> = Vec::new(); let mut arrivals: Vec<(u32, u8)> = Vec::new();
let mut last = None; let mut last = None;
let mut walled_at: Option<u32> = None; let mut walled_at: Option<u32> = 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 { for frame in 0..budget {
palette.clock(ms); palette.clock(ms);
let observed = { let observed = {
let ledger = AdapterLedger(&run.adapter); let ledger = AdapterLedger(&run.adapter);
palette.observe(&mut run.gb, &ledger) 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 mut mask = 0u8;
{ {
let ledger = AdapterLedger(&run.adapter); let ledger = AdapterLedger(&run.adapter);
@ -3774,13 +3791,16 @@ fn a_trainers_challenge_does_not_wall_the_road_to_the_forests_north_gate() {
.map(|(frame, _)| *frame); .map(|(frame, _)| *frame);
let pewter = first(PEWTER); let pewter = first(PEWTER);
eprintln!( eprintln!(
"{:.1} brain minutes: (1, 18) walled at {walled_at:?}; north gate {north_gate:?}, Route 2 \ "{:.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 {}", after it {route_2_north:?}, Pewter City {pewter:?}; pushed {:?}; rank {}; arrivals {}",
(ms - run.ms) / 60_000.0, (ms - run.ms) / 60_000.0,
palette.fences().0, palette.fences().0,
run.adapter.progress().rank, run.adapter.progress().rank,
arrivals.len(), 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_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!(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!(route_2_north.is_some(), "nor Route 2 through it: {arrivals:?}");