flybrain/services/flysim/crates/flysim/tests/rom_macros_mode.rs
acamilo 4edc4c9bcf tests: ROM-gated, the fly gets out of the museum and into the gym
From the live rung-10 checkpoint with the stub rotation: map 0x34 to map 0x36 on frame 2423, on
15 macros, against never in five and a half live hours. TALK is on the pad inside the gym -- the
fly standing in front of one of the people rung 11's place names -- and it presses it 15 times
there. BACK on a dialog or unknown frame is 0 against the 189 the hunt measured on Pewter City
alone, no frame deals a pad on an unknown with nothing drawn on it, and GO FRONTIER starts 0
times on the museum's two floors.

The unit test beside it is the other half of "the objective reaches the leader": on the gym's
own map the rung's place resolves to the person standing in it, the walk aims at the four tiles
around them, the door is not a candidate while they are there, and talking to them empties the
list. It also pins what the ROM run showed: the town's errands come first for every map in the
area, the gym included, so both have to be paid before the objective is the leader.
2026-09-22 13:12:31 +00:00

2383 lines
115 KiB
Rust

//! Macros mode on the real cartridge: the scene sets the buttons, the readout presses one.
//!
//! Gated on `FLY_ROM`, the same convention every ROM test in the workspace uses; the cartridge
//! never enters this repository:
//!
//! ```sh
//! FLY_ROM="$HOME/fly-plays-pokemon/Pokemon Red (U) [S][BF].gb" \
//! cargo test --release -p flysim --test rom_macros_mode -- --nocapture
//! ```
//!
//! ## What only the cartridge can answer
//!
//! Whether the scene's own buttons plus a readout that knows nothing about the game are enough to
//! get out of Red's house (`docs/design/macros.md` section 12). The unit tests pin which macros a
//! scene binds (`pokemon_red::macros`) and what the layer does with a winning channel
//! (`flysim::macros`) separately, and neither can tell you whether the two together reach Oak's
//! lab — that is a question about the warp table, about a doormat that fires on the step off it,
//! and about the rung catalog's places being the ones the ladder actually climbs.
//!
//! ## The stub readout
//!
//! A real [`PopulationDecoder`] with the Game Boy preset's macro group, fed synthetic rates: no
//! brain. One burst per hold leans on a single `macro_<type>` population — 1.6 times its own
//! calibration rate, every other population at rest — and the hot one rotates every
//! [`HOLDS_PER_SLOT`] bursts, which is the shape a settled network's own group has ("not a
//! tie-breaker; it is the rotation period", `docs/readout.md`). Nothing else about the decision is
//! stubbed: the mask of bound channels, the hold, the hysteresis and the fatigue are the shipping
//! decoder's, so what this test measures is the scene's bindings and the layer, with a readout
//! that never names a macro and cannot see the game.
//!
//! The rotation walks all twenty-two channels while a scene binds at most six, so most bursts lean
//! on a channel that is masked out of the decision; that is the honest case and deliberately the
//! weak one, because a masked burst leaves the bound channels tied and the group falls back on its
//! own tie rule. The burst is 100 ms, longer than the group's rise and shorter than its hold, so
//! exactly one decision comes out of each.
use flybrain_core::decoder::PopulationDecoder;
use flybrain_core::decoder::gameboy::gameboy_decoder_config_with_macros;
use flybrain_core::ordered::NumberMap;
use flybrain_gb::pokemon_red::PokemonRedReward;
use flybrain_gb::{
AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, GameAdapter, buttons,
};
use flysim::config::Config;
use flysim::macros::{MacroLayer, macro_layer};
use flysim::snapshot::MacroMode;
/// `constants/map_constants.asm`.
const PALLET_TOWN: u32 = 0x00;
const REDS_HOUSE_1F: u32 = 0x25;
const REDS_HOUSE_2F: u32 = 0x26;
const OAKS_LAB: u32 = 0x28;
const VIRIDIAN_CITY: u32 = 0x01;
const ROUTE_2: u32 = 0x0d;
/// Kept for the record of what the fly used to reach first: before the objective pointed at the
/// errand, the mart's door was the only door out of Viridian the macros could find.
#[allow(dead_code)]
const VIRIDIAN_MART: u32 = 0x2a;
/// Route 2's southern forest gate and the forest north of it, which is rung 9's own road.
const VIRIDIAN_FOREST_SOUTH_GATE: u32 = 0x32;
const VIRIDIAN_FOREST: u32 = 0x33;
/// Pewter City's Pokemon Center, which is the room the rung-10 nurse loop was inside.
const PEWTER_POKECENTER: u32 = 0x3a;
/// The upper floor of the Pewter museum, which is the building the rung-10 stall was inside.
const MUSEUM_2F: u32 = 0x35;
/// Pewter City and its two museum floors and gym: the rung-10 loop's whole world.
const PEWTER_CITY: u32 = 0x02;
const MUSEUM_1F: u32 = 0x34;
const PEWTER_GYM: u32 = 0x36;
/// The forest's *northern* gate, which is the first hop from the forest toward Pewter
/// (`macros::geography`, and rung 10's own road).
const VIRIDIAN_FOREST_NORTH_GATE: u32 = 0x2f;
/// `constants/event_constants.asm`, by bit index: the parcel picked up at the mart, and the parcel
/// delivered to Oak. The save that stalled has the first and not the second.
const EVENT_GOT_OAKS_PARCEL: u16 = 57;
const EVENT_OAK_GOT_PARCEL: u16 = 56;
/// `EVENT_GOT_POKEDEX`, rung 7, which comes out of the same script as the delivery.
///
/// Kept beside its sibling now that the delivery is measured rather than asserted: the two are one
/// script, so a test that ever asserts one will want the other.
#[allow(dead_code)]
const EVENT_GOT_POKEDEX: u16 = 37;
const MS_PER_FRAME: f64 = 1000.0 / 59.7275;
/// Frames of one channel per burst: long enough for one decision, short enough not to be a hold.
const BURST_MS: f64 = 100.0;
/// The seed the bench uses, so a failure here and a bench row are the same run.
const SEED: u32 = 20_260_916;
/// The hot population's rate, against [`REST`] for every other one.
///
/// A normalized score of 1.6, which is far enough above the rest for the group's 1.05 hysteresis
/// to be beatable in one decision and still a rate a real population reaches.
const HOT: f64 = 16.0;
/// Every other population's rate, which is also what the stub calibrates against, so a channel
/// that is not the hot one scores exactly 1.0.
const REST: f64 = 10.0;
/// Bursts the stub leans on one channel before it moves on.
///
/// The shape of the real readout rather than a number picked to pass: the exclusive group commits
/// to a winner, hysteresis keeps it while the field is within 5%, and `fatigue_gain` is what
/// eventually moves it on — "not a tie-breaker; it is the rotation period" (`docs/readout.md`).
/// Three holds is that rotation, coarsely.
const HOLDS_PER_SLOT: usize = 3;
/// The stub's rates for one burst: one macro population hot, the rest at their calibration rate.
///
/// The readout says "this population is firing" and nothing else — it never names a macro, never
/// reads the scene and does not know which channels are on the pad. Everything between that and a
/// macro starting is the shipping decoder and the shipping layer.
fn rates(hot: Option<&str>) -> NumberMap {
let mut rates = NumberMap::new();
for channel in flybrain_gb::macro_channels("pokemon-red") {
rates.set(channel, REST);
}
for bucket in 0..8 {
rates.set(&format!("command_{bucket}"), REST);
}
if let Some(channel) = hot {
rates.set(channel, HOT);
}
rates
}
fn rom() -> Option<Vec<u8>> {
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")
}
/// The cartridge, the adapter beside it, and the layer under test.
struct Run {
gb: Emulator,
adapter: PokemonRedReward,
layer: MacroLayer,
/// The readout under test: the shipping decoder, fed by hand.
decoder: PopulationDecoder,
/// The macro channels, in the decoder's own order, for the rotation.
channels: Vec<&'static str>,
ms: f64,
/// Brain clock of the next burst.
next_burst: f64,
/// Which burst this is, i.e. which channel the stub readout is leaning on.
burst: usize,
hold_ms: f64,
/// Every map the run has been on, in order of arrival.
route: Vec<u32>,
/// How many times each macro has started, for the failure message and the record.
started: std::collections::BTreeMap<&'static str, u32>,
/// Runs of consecutive frames in which `TALK`'s channel was on the pad — a *facing window*,
/// the only chance the fly has to press A at what it is looking at.
talk_windows: u32,
talk_on_pad: bool,
/// A channel to lean on instead of the rotation, for a test that needs one macro pressed.
///
/// The rotation is the honest driver and every other test uses it; this is for the one question
/// that is about a *script* rather than about which button the fly picks.
force_hot: Option<&'static str>,
/// The smallest overworld pad each map ever dealt, by map id.
///
/// A pad of one button is a fly with one thing it can do: on 2026-09-17 the south gate of
/// Viridian Forest dealt `GO FRONTIER` and nothing else for four hours
/// (`infra/docs/macros-traps.md` row 32). Recorded per map rather than asserted here, because
/// which maps a rotation visits is not this harness's claim.
pads: std::collections::BTreeMap<u32, usize>,
/// Whether `ATTACK`'s channel was ever on the pad while the scene was a battle, and the
/// smallest battle pad the run ever dealt. Row 34's own two numbers
/// (`infra/docs/macros-traps.md`): a turn with no move left with PP took `ATTACK` off the pad
/// and the party list it fell into dealt one button.
move_button_on_battle_pad: bool,
battle_pad: Option<usize>,
/// Whether `BACK` was ever on the pad in a battle with **no list open** (section 12.9).
///
/// The rung-9 trap: between turns there is nothing to back out of, so the press completes
/// where the fly stands and the turn does not move. `false` is the assertion.
back_without_a_list: bool,
/// Where a `BACK` or a `NEXT` on a battle frame was dealt, as `scene/sub-state`.
///
/// The bool above says a rule broke; this says on which frame, which is the difference
/// between a pad rule to fix and a scene the detector cannot name.
battle_back_where: std::collections::BTreeSet<String>,
battle_next_where: std::collections::BTreeSet<String>,
/// Whether `THROW BALL` ever started at a species the party already held (section 12.9).
threw_at_a_held_species: bool,
/// Whether one battle pad ever held both `NEXT` and `BACK` (section 12.10).
///
/// The pair is the trap: `NEXT` is the A that advances text, `BACK` is the B that leaves a
/// list, and a pad with both has two buttons that undo each other with nothing else changing.
next_and_back_on_one_pad: bool,
/// Whether `NEXT` was ever on the pad while a battle menu was accepting input.
///
/// The live v0.4.2 shape: `NEXT` on the top-level menu was an A press on FIGHT, so it opened
/// the move list that `BACK` closed again -- 1264 starts against 1241 in 71 hours.
next_on_a_menu_accepting_input: bool,
/// The longest chain of battle macro starts that alternated `NEXT`, `BACK`, `NEXT`, `BACK`.
///
/// Two is an accident of the rotation; the live log did it for seventy-one hours. Only the
/// previous battle start has to be kept to measure the chain.
longest_next_back_alternation: u32,
alternation: u32,
last_battle_start: Option<&'static str>,
/// Macros started inside the battle that is running, and the worst any *finished* battle cost.
///
/// The claim the rung-9 loop breaks is that a battle **ends**, and that it ends on a bounded
/// number of macros rather than on however many holds the 2-cycle takes to fall out of.
macros_this_battle: u32,
worst_battle_macros: u32,
battles_entered: u32,
battles_ended: u32,
was_in_battle: bool,
/// Maps on whose *overworld* pad `GO OBJECTIVE` was ever bound.
///
/// The objective is the road out: on rung 9 in the forest it has to be there, or the only way
/// north is whatever `GO FRONTIER` stumbles into.
objective_on_pad: std::collections::BTreeSet<u32>,
/// Maps on whose *overworld* pad `MENU` was ever bound (section 12.11).
///
/// The rung-10 trap: `MENU` opened the start menu and that scene's `BACK` closed it again, 82
/// starts each in thirty brain minutes inside one building. `MENU` is on no pad at all now, so
/// this set is empty -- and on v0.4.3 it holds every overworld map the run stood on.
menu_on_pad: std::collections::BTreeSet<u32>,
/// Overworld pads that bound nothing at all, by map: section 13.1's never-empty rule, which
/// since 12.11 rests on the way out rather than on an unconditional `MENU`.
empty_overworld_pads: std::collections::BTreeSet<u32>,
/// How many times each macro finished `blocked`, for the `THROW BALL` residual.
///
/// v0.4.3 on the cartridge: `THROW BALL` 63 starts, 63 `blocked` -- the bag had not drawn and
/// the step read the battle menu's cursor instead (section 12.11).
blocked: std::collections::BTreeMap<&'static str, u32>,
/// `NAME@scene/sub-state` for every macro that reported `blocked`: *where* it gave up.
blocked_where: std::collections::BTreeSet<String>,
/// Macros started while the fly was still on the map it resumed on.
macros_on_the_first_map: u32,
/// How many times each macro started while the fly was still on that map, by name.
///
/// The total is the wrong measure for a room the fly is meant to *leave*: a run that leaves it
/// and then fights a gym answers `YES` in the gym's own boxes, which is the fly playing the
/// game. What row 41 is about is the presses spent in the room.
started_on_the_first_map: std::collections::BTreeMap<&'static str, u32>,
/// The longest chain of macro starts that alternated `MENU`, `BACK`, `MENU`, `BACK`.
longest_menu_back_alternation: u32,
menu_alternation: u32,
last_start: Option<&'static str>,
/// Frames the run spent in a `Scene::Dialog`, and of those, frames a readable YES/NO prompt
/// was open (section 12.12).
///
/// The two numbers that name row 41: 62,804 of the hunt's 71,673 frames were one text box, and
/// the survey found the **prompt** on one frame of every forty-six. A `NEXT` and a `YES` that
/// are the same press live in the difference.
dialog_frames: u32,
prompt_frames: u32,
/// Whether `NEXT` was ever on the pad while a readable YES/NO prompt was open.
///
/// 12.10's rule in a dialog: an A press at a two-option box confirms the option the cursor is
/// on, which is what `YES` is, so the two are one press under two names. `false` is the claim.
next_on_a_prompt: bool,
/// Whether `TALK` was ever on the pad while the fly faced a nurse the party had no use for.
///
/// The door into the ring (section 12.12). `false` is the claim.
talk_at_a_rested_nurse: bool,
/// `BACK` starts on a frame the scene detector called `dialog` or `unknown` (section 12.13).
///
/// The same attribution `examples/trap_hunt.rs` makes, so the number is comparable with the
/// residual it comes from: **189** `BACK` in a text box on map `0x02` in twenty brain
/// minutes, and 678 starts in the 47 live minutes. `BACK` is on no overworld and no dialog
/// pad, so every one of them was dealt by `Unknown`.
back_in_a_box: u32,
/// Frames where the scene was `unknown`, nothing was drawn, and the pad was **not** empty.
///
/// Section 12.13's claim, and it is zero: a frame of the overworld the cartridge is driving
/// has nothing an A or a B press can do, so it deals nothing and the fly waits.
unknown_pads_with_no_box: u32,
/// Maps on whose overworld pad `TALK` was ever bound: where the fly stood facing somebody
/// this run has not talked to.
///
/// Rung 11 is BOULDER BADGE and its place is a *person* in map `0x36`, so "the fly reaches
/// the leader" is this map being in the set: the fly inside the gym, in front of one of its
/// people, with the press that talks to them on the pad. Whether it presses is the fly's
/// (section 12). Read off the shipping pad rather than recomputed, so it cannot disagree
/// with what the fly was actually offered.
talk_on_pad_by_map: std::collections::BTreeSet<u32>,
/// `TALK` starts by map, for the record beside it.
talk_starts_by_map: std::collections::BTreeMap<u32, u32>,
/// `GO FRONTIER` starts by map, for the museum (section 12.14).
frontier_by_map: std::collections::BTreeMap<u32, u32>,
}
impl Run {
/// Boot the intro with scripted presses, then hand the buttons to macros mode.
///
/// The button pattern is `tests/rom.rs`'s and the bench's: Start and A on a slow alternation,
/// because a held button is ignored, then B alone until the adapter reports a stable
/// dialogue-free overworld sample.
fn boot(rom: &[u8], mode: MacroMode) -> Self {
let mut gb = emulator(rom);
let mut adapter = PokemonRedReward::new();
let mut ms = 0.0;
for frame in 0..6_000u32 {
let mask = match frame % 32 {
0..=7 => buttons::START,
16..=23 => buttons::A,
_ => buttons::NONE,
};
gb.set_buttons(mask);
gb.run_frame().expect("a frame should complete");
ms += MS_PER_FRAME;
adapter.sample(&mut gb, ms);
if adapter.mode() == "OVERWORLD" && frame > 3_000 {
break;
}
}
for frame in 0..12_000u32 {
gb.set_buttons(if frame % 24 < 8 { buttons::B } else { buttons::NONE });
gb.run_frame().expect("a frame should complete");
ms += MS_PER_FRAME;
adapter.sample(&mut gb, ms);
if adapter.safe_for_snapshot() {
break;
}
}
assert!(adapter.safe_for_snapshot(), "the intro never settled (mode {})", adapter.mode());
assert_eq!(adapter.map_id(), Some(REDS_HOUSE_2F), "a cold boot ends in bed");
let channels = flybrain_gb::macro_channels("pokemon-red");
let preset = gameboy_decoder_config_with_macros(&channels);
let hold_ms = preset.macros.as_ref().expect("the preset has a macro group").hold_ms;
let mut decoder = PopulationDecoder::new(preset).expect("the preset is well formed");
// Calibrated on the resting rates, so a channel at `REST` scores exactly 1.0 and the hot
// one 1.6.
decoder.calibrate(&rates(None));
let mut config = Config::default();
config.loop_.game = "pokemon-red".to_string();
config.macros.mode = mode;
config.validate().expect("pokemon-red has a palette in macros mode");
let mut layer = macro_layer(&config, hold_ms, SEED).expect("a layer in a dealt mode");
let _ = layer.observe(&mut gb, &AdapterLedger(&adapter), ms);
let route = vec![REDS_HOUSE_2F];
Self {
gb,
adapter,
layer,
decoder,
channels,
ms,
next_burst: ms,
burst: 0,
hold_ms,
route,
started: std::collections::BTreeMap::new(),
talk_windows: 0,
talk_on_pad: false,
force_hot: None,
pads: std::collections::BTreeMap::new(),
move_button_on_battle_pad: false,
battle_pad: None,
back_without_a_list: false,
battle_back_where: std::collections::BTreeSet::new(),
battle_next_where: std::collections::BTreeSet::new(),
threw_at_a_held_species: false,
next_and_back_on_one_pad: false,
next_on_a_menu_accepting_input: false,
longest_next_back_alternation: 0,
alternation: 0,
last_battle_start: None,
macros_this_battle: 0,
worst_battle_macros: 0,
battles_entered: 0,
battles_ended: 0,
was_in_battle: false,
objective_on_pad: std::collections::BTreeSet::new(),
menu_on_pad: std::collections::BTreeSet::new(),
empty_overworld_pads: std::collections::BTreeSet::new(),
blocked: std::collections::BTreeMap::new(),
blocked_where: std::collections::BTreeSet::new(),
macros_on_the_first_map: 0,
started_on_the_first_map: std::collections::BTreeMap::new(),
longest_menu_back_alternation: 0,
dialog_frames: 0,
prompt_frames: 0,
next_on_a_prompt: false,
talk_at_a_rested_nurse: false,
back_in_a_box: 0,
unknown_pads_with_no_box: 0,
talk_on_pad_by_map: std::collections::BTreeSet::new(),
talk_starts_by_map: std::collections::BTreeMap::new(),
frontier_by_map: std::collections::BTreeMap::new(),
menu_alternation: 0,
last_start: None,
}
}
/// Pick up a run from a `FLYSIM01` checkpoint instead of booting the intro.
///
/// The cartridge state and the reward ledger come out of the envelope; the readout does not,
/// because the point of this harness is a stub that knows nothing about the game. So this is
/// the release box's own world with a game-blind driver in it, which is exactly what a stall
/// on the stream has to be reproduced against.
fn resume(rom: &[u8], mode: MacroMode, checkpoint: &flysim::store::Checkpoint) -> Self {
Self::resume_with(rom, mode, checkpoint, false)
}
/// [`Run::resume`], with `predates_macro_roles` restoring a decoder state that has no baseline
/// for any macro role — the shape the release box restored on 2026-09-17.
fn resume_with(
rom: &[u8],
mode: MacroMode,
checkpoint: &flysim::store::Checkpoint,
predates_macro_roles: bool,
) -> Self {
let mut gb = emulator(rom);
let mut adapter = PokemonRedReward::new();
gb.import_state(&checkpoint.runtime.emulator).expect("the checkpoint's emulator state");
adapter.import_state(&checkpoint.runtime.reward).expect("the checkpoint's reward ledger");
let channels = flybrain_gb::macro_channels("pokemon-red");
let preset = gameboy_decoder_config_with_macros(&channels);
let hold_ms = preset.macros.as_ref().expect("the preset has a macro group").hold_ms;
let mut decoder = PopulationDecoder::new(preset).expect("the preset is well formed");
decoder.calibrate(&rates(None));
let mut config = Config::default();
config.loop_.game = "pokemon-red".to_string();
config.macros.mode = mode;
config.validate().expect("pokemon-red has a palette in macros mode");
let mut layer = macro_layer(&config, hold_ms, SEED).expect("a layer in a dealt mode");
// **The live restore, in this harness's own units.** The release box calibrates at warm-up
// and then imports a checkpoint's decoder state; a checkpoint written before the macro
// roles existed carries no baseline for them, and an absent baseline reads as zero
// (`docs/readout.md`, "Channels added after a checkpoint was written").
//
// The checkpoint's *own* decoder state cannot be used for this: its baselines are the live
// network's population rates and this readout is a stub feeding synthetic ones, so mixing
// them measures neither. What reproduces the defect faithfully is the same state shape in
// the stub's units — calibrated on the stub's rates, with every macro role's baseline
// removed — which is exactly what the live box restored.
if predates_macro_roles {
let mut predates = decoder.export_state();
for channel in &channels {
predates.baseline.remove(channel);
}
decoder.import_state(&predates).expect("a state from before the macro roles");
}
let ms = 0.0;
let _ = layer.observe(&mut gb, &AdapterLedger(&adapter), ms);
let route = vec![adapter.map_id().unwrap_or(u32::MAX)];
Self {
gb,
adapter,
layer,
decoder,
channels,
ms,
next_burst: ms,
burst: 0,
hold_ms,
route,
started: std::collections::BTreeMap::new(),
talk_windows: 0,
talk_on_pad: false,
force_hot: None,
pads: std::collections::BTreeMap::new(),
move_button_on_battle_pad: false,
battle_pad: None,
back_without_a_list: false,
battle_back_where: std::collections::BTreeSet::new(),
battle_next_where: std::collections::BTreeSet::new(),
threw_at_a_held_species: false,
next_and_back_on_one_pad: false,
next_on_a_menu_accepting_input: false,
longest_next_back_alternation: 0,
alternation: 0,
last_battle_start: None,
macros_this_battle: 0,
worst_battle_macros: 0,
battles_entered: 0,
battles_ended: 0,
was_in_battle: false,
objective_on_pad: std::collections::BTreeSet::new(),
menu_on_pad: std::collections::BTreeSet::new(),
empty_overworld_pads: std::collections::BTreeSet::new(),
blocked: std::collections::BTreeMap::new(),
blocked_where: std::collections::BTreeSet::new(),
macros_on_the_first_map: 0,
started_on_the_first_map: std::collections::BTreeMap::new(),
longest_menu_back_alternation: 0,
dialog_frames: 0,
prompt_frames: 0,
next_on_a_prompt: false,
talk_at_a_rested_nurse: false,
back_in_a_box: 0,
unknown_pads_with_no_box: 0,
talk_on_pad_by_map: std::collections::BTreeSet::new(),
talk_starts_by_map: std::collections::BTreeMap::new(),
frontier_by_map: std::collections::BTreeMap::new(),
menu_alternation: 0,
last_start: None,
}
}
fn map(&self) -> u32 {
self.adapter.map_id().unwrap_or(u32::MAX)
}
/// One of pokered's event flags, by bit index (`pokemon_red::symbols::events`).
///
/// The ladder's rank cannot answer for a rung earned out of order — it is the *maximum* over
/// satisfied rungs, so delivering the parcel while Viridian City is already stood on leaves it
/// at 8 — so a test about the delivery asks the bit the delivery sets.
fn event(&mut self, bit: u16) -> bool {
let address = flybrain_gb::pokemon_red::symbols::ram::wEventFlags + (bit >> 3);
self.gb.read_wram(address) & (1 << (bit & 7)) != 0
}
/// Whether a battle menu is accepting input, i.e. the fly's own turn
/// (`docs/design/macros.md` section 12.6). Read through the same seam the macros read.
fn own_turn(&mut self) -> bool {
use flybrain_gb::pokemon_red::macros::state::GameState;
let ledger = AdapterLedger(&self.adapter);
let mut state =
flybrain_gb::pokemon_red::state::PokeState::with_ledger(&mut self.gb, &ledger);
state.battle().is_some_and(|battle| battle.own_turn && !battle.forced_switch)
}
/// Whether a battle list is open and accepting input: the move list, the party list or the bag.
///
/// Section 12.9's question, through the same seam the macros read: `BACK` is a button where
/// there is a list to leave and nowhere else.
/// Whether the pad on this frame is a **battle** pad.
///
/// The scene the palette was dealt for, not `wIsInBattle`. The two differ on the `$ff` frame a
/// lost battle passes through and on a Safari or tutorial battle, where `state::battle` reads
/// nothing and `scene::detect` answers `Unknown` -- whose pad is `NEXT, BACK` by contract
/// (section 12.2, row 9: B is what leaves the Pokédex, the trainer card and OPTION). Asking
/// the cartridge byte instead accused that row of a battle rule it is not under.
fn battle_pad(&self) -> bool {
self.layer.scene_name() == "battle" || self.layer.scene_name() == "battle-switch"
}
/// The battle sub-state the seam reports, as a word, for the record and the failure message.
fn battle_sub_state(&mut self) -> &'static str {
use flybrain_gb::pokemon_red::macros::state::{BattleMenu, GameState};
let ledger = AdapterLedger(&self.adapter);
let mut state =
flybrain_gb::pokemon_red::state::PokeState::with_ledger(&mut self.gb, &ledger);
match state.battle() {
None => "no-battle",
Some(battle) => match battle.menu {
BattleMenu::Main { .. } => "main",
BattleMenu::Moves { cursor: Some(_), .. } => "moves",
BattleMenu::Moves { cursor: None, .. } => "moves-unplaceable",
BattleMenu::Party { .. } if battle.forced_switch => "party-forced",
BattleMenu::Party { .. } => "party",
BattleMenu::Bag { .. } => "bag",
BattleMenu::None => "between-turns",
},
}
}
fn battle_list_open(&mut self) -> bool {
use flybrain_gb::pokemon_red::macros::state::{BattleMenu, GameState};
let ledger = AdapterLedger(&self.adapter);
let mut state =
flybrain_gb::pokemon_red::state::PokeState::with_ledger(&mut self.gb, &ledger);
state.battle().is_some_and(|battle| {
matches!(
battle.menu,
BattleMenu::Moves { cursor: Some(_), .. }
| BattleMenu::Party { .. }
| BattleMenu::Bag { .. }
)
})
}
/// Whether the Pokémon on the other side is a species the party already holds.
///
/// The internal species index on both sides, which is the one numbering they share
/// (`docs/design/macros-wram.md`).
fn enemy_species_in_party(&mut self) -> bool {
use flybrain_gb::pokemon_red::macros::state::GameState;
let ledger = AdapterLedger(&self.adapter);
let mut state =
flybrain_gb::pokemon_red::state::PokeState::with_ledger(&mut self.gb, &ledger);
let Some(species) =
state.battle().and_then(|battle| battle.enemy).map(|enemy| enemy.species)
else {
return false;
};
species != 0 && state.party().mons.iter().any(|mon| mon.species == species)
}
/// The rung the macros are walking toward, as the objective reads it.
fn objective_map(&mut self) -> Option<u8> {
use flybrain_gb::pokemon_red::macros::MacroState;
let ledger = AdapterLedger(&self.adapter);
let mut state =
flybrain_gb::pokemon_red::state::PokeState::with_ledger(&mut self.gb, &ledger);
state.objective().map(|objective| objective.map)
}
/// `wIsInBattle`: 0 out of battle, 1 wild, 2 a trainer.
fn in_battle(&mut self) -> u8 {
self.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wIsInBattle)
}
/// `wPartyCount`, which is what taking a starter changes.
fn party(&mut self) -> u8 {
self.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wPartyCount)
}
/// The wallet, in whole units, through the same accessor the macros read it with.
fn money(&mut self) -> u32 {
flybrain_gb::pokemon_red::state::money(&mut self.gb)
}
/// How many of `item` the bag holds, summed over its stacks.
///
/// Kept unused for now: the purchase the section-13 test would have counted with it is
/// unit-proven rather than ROM-proven (see `go_shop_enters_the_mart_once_and_walks_to_its_counter`
/// and rows 35 and 36 of `infra/docs/macros-traps.md`), and deleting the reader would mean
/// writing it again with the fix.
#[allow(dead_code)]
fn bag_count(&mut self, item: u8) -> u32 {
flybrain_gb::pokemon_red::state::bag(&mut self.gb)
.iter()
.filter(|stack| stack.id == item)
.map(|stack| u32::from(stack.count))
.sum()
}
/// What the open counter sells, through the same gated accessor the macros read it with.
///
/// Empty off a counter, and that gate is the point: `wItemList` is a scratch buffer nobody
/// clears, so a non-empty answer *is* "a real mart is open" -- which is a stronger test than
/// the scene name. Measured on the cartridge 2026-09-17: a text box in Viridian City with a
/// stale `wTextBoxID` reads as `Scene::Shop` and this still answers `[]`.
fn counter_stock(&mut self) -> Vec<u8> {
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(&mut self.gb);
flybrain_gb::pokemon_red::macros::MacroState::shop_stock(&mut state)
}
/// Whether the two-option YES/NO box is drawn, through the accessor the palette reads
/// (section 12.12).
fn yes_no_prompt(&mut self) -> bool {
flybrain_gb::pokemon_red::state::yes_no_prompt(&mut self.gb)
}
/// Whether the fly faces a Pokemon Center nurse with a party that does not need her.
fn rested_nurse(&mut self) -> bool {
let ledger = AdapterLedger(&self.adapter);
let mut state =
flybrain_gb::pokemon_red::state::PokeState::with_ledger(&mut self.gb, &ledger);
flybrain_gb::pokemon_red::macros::palette::rested_nurse(&mut state)
}
/// Whether every party member reads full HP with no status: the end of a `HEAL`.
fn party_rested(&mut self) -> bool {
let party = flybrain_gb::pokemon_red::state::party(&mut self.gb);
!party.mons.is_empty()
&& party.mons.iter().all(|mon| {
mon.species == 0
|| (mon.hp == mon.max_hp
&& mon.status == flybrain_gb::pokemon_red::macros::state::Status::Healthy)
})
}
/// One frame of the sim loop's order, with the stub rates in place of the brain.
fn frame(&mut self) {
// The stub readout: one burst per hold, one macro population hot in it, rotating.
let bursting = self.ms < self.next_burst + BURST_MS;
let hot = bursting.then(|| {
self.force_hot
.unwrap_or(self.channels[(self.burst / HOLDS_PER_SLOT) % self.channels.len()])
});
if self.ms >= self.next_burst + self.hold_ms {
self.next_burst = self.ms;
self.burst += 1;
}
// The loop's own two lines: the scene's bound channels are the mask, and the decoder
// picks among exactly those (`docs/design/macros.md` section 12).
let bound = self.layer.bound_channels();
// Only the fly's *own* turn. Battle text between turns is a pad of one `NEXT` by design
// (the v0.2.4 deadlock fix), so counting it would measure the wrong thing.
if self.own_turn() {
self.move_button_on_battle_pad |=
bound.iter().any(|channel| channel.starts_with("macro_move_"));
let dealt = bound.len();
self.battle_pad = Some(self.battle_pad.map_or(dealt, |seen| seen.min(dealt)));
}
// Section 12.9, on the cartridge: `BACK` belongs to a list. A battle frame with no list
// accepting input and `BACK` on the pad is the rung-9 trap itself.
if self.battle_pad() {
let back = bound.iter().any(|channel| channel.as_str() == "macro_back");
if back {
let where_ = format!("{}/{}", self.layer.scene_name(), self.battle_sub_state());
self.battle_back_where.insert(where_);
}
if back && !self.battle_list_open() {
self.back_without_a_list = true;
}
}
// Section 12.10, on the cartridge: no battle pad holds a pair that undoes itself, and
// `NEXT` is on no frame with a cursor accepting input. The second is the stronger of the
// two -- the live pair was split across two sub-states, so no single pad held both.
if self.battle_pad() {
let next = bound.iter().any(|channel| channel.as_str() == "macro_next");
let back = bound.iter().any(|channel| channel.as_str() == "macro_back");
self.next_and_back_on_one_pad |= next && back;
if next {
let where_ = format!("{}/{}", self.layer.scene_name(), self.battle_sub_state());
self.battle_next_where.insert(where_);
}
if next && self.own_turn() {
self.next_on_a_menu_accepting_input = true;
}
}
// A facing window opens when `TALK`'s channel joins the pad and closes when it leaves.
let talk_bound = bound.iter().any(|channel| channel.ends_with("talk"));
if talk_bound && !self.talk_on_pad {
self.talk_windows += 1;
}
self.talk_on_pad = talk_bound;
let active =
self.decoder.decode_bound(&rates(hot), self.ms, false, None, Some(&bound));
let (mask, started, blocked) = {
let ledger = AdapterLedger(&self.adapter);
let decision = self.layer.decide(&active, 0, self.ms, &mut self.gb, &ledger);
let started: Vec<&'static str> = decision
.events
.iter()
.filter(|event| event.outcome.is_none())
.map(|event| event.name)
.collect();
let blocked: Vec<&'static str> = decision
.events
.iter()
.filter(|event| {
event.outcome.is_some_and(|outcome| outcome.as_str() == "blocked")
})
.map(|event| event.name)
.collect();
(decision.mask, started, blocked)
};
for name in blocked {
*self.blocked.entry(name).or_insert(0) += 1;
let sub = self.battle_sub_state();
let list = self.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wListMenuID);
self.blocked_where.insert(format!(
"{name}@{}/{sub}/list={list:#04x}",
self.layer.scene_name()
));
}
let in_battle_now = self.in_battle() != 0;
let on_a_battle_pad = self.battle_pad();
for name in started {
// Section 12.9's other half: a ball is never thrown at a species the party holds.
if name == "THROW BALL" && self.enemy_species_in_party() {
self.threw_at_a_held_species = true;
}
// Section 12.10's own signature, as the live event log printed it: `NEXT start/done,
// BACK start/done`, every hold, for seventy-one hours. Measured as the longest chain
// of consecutive battle starts drawn from those two alone and strictly alternating.
if in_battle_now {
self.macros_this_battle += 1;
}
if on_a_battle_pad {
let two = name == "NEXT" || name == "BACK";
self.alternation = match self.last_battle_start {
Some(last) if two && (last == "NEXT" || last == "BACK") && last != name => {
self.alternation.max(1) + 1
}
_ if two => 1,
_ => 0,
};
self.longest_next_back_alternation =
self.longest_next_back_alternation.max(self.alternation);
self.last_battle_start = Some(name);
}
// Section 12.11's own signature, as the live event log printed it: `MENU
// start/done, BACK start/done`, every hold, for thirty brain minutes inside one
// building. The pair is split across two *scenes*, so this is measured over every
// start rather than over a battle's.
let two = name == "MENU" || name == "BACK";
self.menu_alternation = match self.last_start {
Some(last) if two && (last == "MENU" || last == "BACK") && last != name => {
self.menu_alternation.max(1) + 1
}
_ if two => 1,
_ => 0,
};
self.longest_menu_back_alternation =
self.longest_menu_back_alternation.max(self.menu_alternation);
self.last_start = Some(name);
if self.route.len() == 1 {
self.macros_on_the_first_map += 1;
*self.started_on_the_first_map.entry(name).or_insert(0) += 1;
}
*self.started.entry(name).or_insert(0) += 1;
// Section 12.13: which of the two `Unknown`s a `BACK` was dealt on, attributed
// exactly as the trap hunt attributes it so the numbers can be compared.
if name == "BACK" && matches!(self.layer.scene_name(), "dialog" | "unknown") {
self.back_in_a_box += 1;
}
if name == "GO FRONTIER" {
let map = self.map();
*self.frontier_by_map.entry(map).or_insert(0) += 1;
}
if name == "TALK" {
let map = self.map();
*self.talk_starts_by_map.entry(map).or_insert(0) += 1;
}
}
self.gb.set_buttons(mask as u8);
self.gb.run_frame().expect("a frame should complete");
self.ms += MS_PER_FRAME;
let ms = self.ms;
self.adapter.sample(&mut self.gb, ms);
{
let ledger = AdapterLedger(&self.adapter);
let _ = self.layer.observe(&mut self.gb, &ledger, ms);
}
// Battle boundaries, after the frame: what a battle cost in macros, and whether it ended.
let now_in_battle = self.in_battle() != 0;
match (self.was_in_battle, now_in_battle) {
(false, true) => {
self.battles_entered += 1;
self.macros_this_battle = 0;
self.last_battle_start = None;
self.alternation = 0;
}
(true, false) => {
self.battles_ended += 1;
self.worst_battle_macros =
self.worst_battle_macros.max(self.macros_this_battle);
self.macros_this_battle = 0;
}
_ => {}
}
self.was_in_battle = now_in_battle;
let map = self.map();
if self.route.last() != Some(&map) && map != u32::MAX {
self.route.push(map);
}
// Section 12.12's two frame counters and its two pad rules, asked of the frame the pad
// was dealt for -- the dialog's, exactly as 12.10 asks the battle rules of the battle's.
if self.layer.scene_name() == "dialog" {
self.dialog_frames += 1;
let dealt = self.layer.bound_channels();
if self.yes_no_prompt() {
self.prompt_frames += 1;
if dealt.iter().any(|channel| channel.as_str() == "macro_next") {
self.next_on_a_prompt = true;
}
}
}
// Section 12.13's claim, per frame: an `Unknown` with nothing drawn deals nothing.
if self.layer.scene_name() == "unknown"
&& !flybrain_gb::pokemon_red::state::text_box(&mut self.gb).open
&& !self.layer.bound_channels().is_empty()
{
self.unknown_pads_with_no_box += 1;
}
if self.layer.scene_name() == "overworld"
&& self.rested_nurse()
&& self.layer.bound_channels().iter().any(|channel| channel.as_str() == "macro_talk")
{
self.talk_at_a_rested_nurse = true;
}
// The pad the *next* frame will choose from, for the maps that have been accused of
// dealing one button. Only the overworld: a warp in flight reads `unknown` and a text box
// is a pad of its own.
if map != u32::MAX && self.layer.scene_name() == "overworld" {
let dealt = self.layer.bound_channels();
if dealt.iter().any(|channel| channel.as_str() == "macro_go_objective") {
self.objective_on_pad.insert(map);
}
if dealt.iter().any(|channel| channel.as_str() == "macro_menu") {
self.menu_on_pad.insert(map);
}
// Standing in front of somebody this run has not talked to, which on the gym's own
// map is standing in front of one of the people rung 11's place names.
if dealt.iter().any(|channel| channel.as_str() == "macro_talk") {
self.talk_on_pad_by_map.insert(map);
}
if dealt.is_empty() {
self.empty_overworld_pads.insert(map);
}
let dealt = dealt.len();
let seen = self.pads.entry(map).or_insert(dealt);
*seen = (*seen).min(dealt);
}
}
/// Drive until `done`, or panic with where it got to.
fn drive_until(&mut self, what: &str, budget: u32, mut done: impl FnMut(&mut Self) -> bool) {
for frame in 0..budget {
self.frame();
if done(self) {
eprintln!(
"{what}: reached after {frame} frames ({:.1} brain minutes), route {:?}, \
macros {:?}",
self.ms / 60_000.0,
self.route,
self.layer.counts()
);
return;
}
}
panic!(
"{what}: not reached in {budget} frames (map {:#04x}, rank {}, tile {:?}, route {:?}, \
macros {:?}, by name {:?}, smallest pads {:?})",
self.map(),
self.adapter.progress().rank,
(
self.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wXCoord),
self.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wYCoord)
),
self.route,
self.layer.counts(),
self.started,
self.pads
);
}
}
macro_rules! skip_without_rom {
() => {
match rom() {
Some(rom) => rom,
None => {
eprintln!("skipped: FLY_ROM is not set");
return;
}
}
};
}
/// From the bedroom, macros mode with a stub readout leaves the house and earns Oak's lab.
///
/// The three rungs the catalog knows places for at this end of the ladder, in order: the ground
/// floor (rung 2, a passage on this map), Pallet Town (rung 3, `LAST_MAP` doormats, which count
/// because the objective is outdoors) and Oak's lab (rung 4, a Pallet Town warp that names the
/// map). The readout knows nothing about any of it — it leans on one population at a time and
/// rotates — so what carries the run is which buttons each scene puts on the pad and what the
/// macros behind them do. If a place were missing from the catalog, or a scene bound the wrong
/// set, this would wander instead.
///
/// **The starter is not part of this test, and that is a finding rather than a trim**
/// (2026-09-16, recorded in `infra/docs/macros-bench.md`). A game-blind stub does not press `TALK`
/// in front of a Pokéball on purpose: three of them were tried on the cartridge under the drive
/// rules section 12 replaced and none took a starter — every slot equal pressed A 615 times at one
/// shelf and never walked away; one hot slot per burst wandered (367 `GO OUT`s, one `TALK`); a
/// committing rotation did the same over five times the frames (4,904 macros, `TALK` once). What
/// a stub cannot do is exactly what section 12 hands to the mushroom body, so the claim belongs to
/// a run with a brain in it — the bench's macros arm — and not here.
#[test]
fn macros_mode_leaves_the_house_and_reaches_oaks_lab() {
let rom = skip_without_rom!();
let mut run = Run::boot(&rom, MacroMode::Macros);
run.drive_until("the ground floor", 12_000, |run| run.map() == REDS_HOUSE_1F);
run.drive_until("pallet town", 24_000, |run| run.map() == PALLET_TOWN);
run.drive_until("oak's lab", 60_000, |run| run.map() == OAKS_LAB);
// The rung is not the arrival. A map rung needs three stable unscripted samples and the fly
// lands *on* the lab's own door, which the adapter's gate rejects by design
// (`docs/design/room-escape.md` section 2), so the rung is earned once another macro has
// walked it into the room.
run.drive_until("the lab's own rung", 24_000, |run| run.adapter.progress().rank >= 4);
assert!(run.route.contains(&REDS_HOUSE_1F), "through the ground floor: {:?}", run.route);
assert!(run.route.contains(&PALLET_TOWN), "and out into the town: {:?}", run.route);
assert_eq!(run.map(), OAKS_LAB);
assert!(run.adapter.progress().rank >= 4, "the ladder recorded it: {:?}", run.adapter.progress());
assert_eq!(run.party(), 0, "nothing here takes a starter; see this test's own note");
// Every macro that ran was one the scene had put on the pad and the readout had held up: the
// counts are the proof that the two halves met.
let counts = run.layer.counts();
assert!(counts.started > 0, "no macro ever started: {counts:?}");
assert_eq!(counts.started, counts.finished() + u64::from(run.layer.running().is_some()));
eprintln!(
"macros mode reached Oak's lab in {:.1} brain minutes on {} macros: {counts:?}",
run.ms / 60_000.0,
counts.started
);
}
/// The checkpoint a resumed test starts from, or `None` to skip.
///
/// `.local/` is not tracked and a checkpoint is not a fixture: it is the state the release box was
/// really in. So this reads the same variables `examples/trap_hunt.rs` does and skips without one.
fn checkpoint() -> Option<flysim::store::Checkpoint> {
let path = std::env::var_os("FLY_TRAP_CHECKPOINT")
.or_else(|| std::env::var_os("FLY_MACRO_CHECKPOINT"))?;
Some(
flysim::store::load(std::path::Path::new(&path))
.expect("the checkpoint should be a FLYSIM01 envelope"),
)
}
/// From the Viridian stall's own checkpoint, macros mode's walks complete and reach a door.
///
/// **The stall** (2026-09-17, `infra/docs/macros-traps.md`): eight hours on rung 8, 164 walk starts
/// and 165 timeouts in the last 40 KB of the event log, and not one macro finishing. Every walk
/// spent its whole 600-frame cap oscillating over *two* tiles with a net displacement of zero,
/// because `path::route`'s closest-approach answer plus a re-plan after every tile traded the fly
/// between two tiles across the walkable window's edge; the cap then read as "closing" and cost a
/// strike instead of excluding anything, so the same walk came back ten brain minutes later for
/// ever.
///
/// **The criterion is that walks complete**, not that they arrive anywhere in particular. The fly
/// chooses the macro and this readout is a rotation that knows nothing about the game, so which
/// target it spends a hold on is not this test's business. What is: that a chosen walk can finish,
/// that finishing is the common case rather than the frame cap, and that the walks carry the fly
/// far enough across Viridian City to reach one of its doors — the mart
/// (`maps::VIRIDIAN_MART`), which is a warp on the far side of the city from the checkpoint's tile.
///
/// **Route 2 is not asserted, and that is a measurement rather than a trim.** `maps::ROUTE_2` is
/// the connection north and rung 9's first hop, so it is what `GO OBJECTIVE` aims at; with these
/// fixes the fly reaches the mart in 3.2 brain minutes and then **never leaves the city** in seven
/// brain hours (1,500,000 frames, 30,946 macros, 30,921 of them `done`). It ends every run in a
/// text box two tiles from where it got stuck, and the `Dialog` pad's `NEXT`, `YES` and `NO` — one
/// A press and one B press — do not advance it. That is the next trap, not this one, and it is
/// recorded as such in `infra/docs/macros-traps.md`; asserting Route 2 here would be a test that
/// fails on a trap it does not measure.
///
/// Gated on `FLY_ROM` *and* on a checkpoint, and skips cleanly without either, because `.local/` is
/// not tracked and a checkpoint is not a fixture — it is the state the release box was really in:
///
/// ```sh
/// FLY_ROM="$HOME/fly-plays-pokemon/Pokemon Red (U) [S][BF].gb" \
/// FLY_TRAP_CHECKPOINT=.local/checkpoints/release-viridian-timeouts.checkpoint \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture
/// ```
#[test]
fn macros_mode_completes_its_walks_from_the_stalled_checkpoint() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
assert_eq!(run.map(), VIRIDIAN_CITY, "the checkpoint is the one the stream stalled on");
// Off the map the stall was on, by whichever door the fly's own choices find. Bounded at
// 120,000 frames, which is 33 brain minutes; the measured run needs 1,600 of them.
//
// **Not a particular door any more.** When this test was written the objective pointed north
// through a shut gate, so the mart was the reachable one; since the objective is the lowest
// *unsatisfied* rung it points at Oak's lab, and the fly leaves southward by Route 1. Which
// door is not this test's claim — that the walks carry the fly off the map at all is.
run.drive_until("off the map the stall was on", 120_000, |run| {
run.route.iter().any(|map| *map != VIRIDIAN_CITY)
});
assert!(run.route.len() > 1, "route {:?}", run.route);
// Then keep going, because the claim is about the *outcomes* rather than the arrival: the
// stall's signature was every walk spending its whole cap and nothing finishing, and one
// macro's worth of frames cannot show that either way.
for _ in 0..30_000 {
run.frame();
}
let counts = run.layer.counts();
assert!(counts.started > 0, "no macro ever started: {counts:?}");
// The stall's signature, and what this test exists to keep out: every walk timing out and
// nothing finishing at all.
assert!(counts.done > 0, "no macro completed: {counts:?}");
assert!(
counts.done > counts.timeout,
"finishing is the common case, not the frame cap: {counts:?}"
);
// On a bounded number of macros rather than by grinding: a budget that scales with the plan
// means one walk covers what took dozens of ten-second ones.
assert!(counts.started < 1_000, "macros spent leaving the map: {counts:?}");
eprintln!(
"off the stalled map in {:.1} brain minutes on {} macros ({counts:?}), route {:?}, \
by name {:?}",
run.ms / 60_000.0,
counts.started,
run.route,
run.started
);
}
/// The rung-9 forest checkpoint, or `None` to skip.
///
/// Its own variable rather than `FLY_TRAP_CHECKPOINT`, because the two checkpoint tests above
/// assert the map their checkpoint is on: one envelope cannot be both.
fn forest_checkpoint() -> Option<flysim::store::Checkpoint> {
let path = std::env::var_os("FLY_FOREST_CHECKPOINT")?;
Some(
flysim::store::load(std::path::Path::new(&path))
.expect("the checkpoint should be a FLYSIM01 envelope"),
)
}
/// From the rung-9 forest checkpoint: the turns advance, the battles end, and no two buttons on a
/// battle pad undo each other.
///
/// **What was live** (2026-09-22, `infra/docs/macros-traps.md`): rank 9, VIRIDIAN FOREST, 69 hours
/// on the rung, the ratchet's three attempts spent, and since the restart the macro starts were
/// `BACK` 135, `THROW BALL` 28, `MOVE 2` 10, `RUN` 5 — the event log repeating `RUN blocked, BACK
/// start, BACK done`. `BACK` on a battle frame with no list open completes in a handful of frames
/// without changing anything, so the roll landed on it most holds and the turn did not move; and
/// `THROW BALL` spent balls on the species already in the party, each catch opening a nickname
/// screen the pad cannot leave.
///
/// **What was live again** (2026-09-22, thirty-five minutes after v0.4.2): the same rung, the same
/// forest, and the macro starts since the restart were `NEXT` 1264, `BACK` 1241, `THROW BALL` 5 and
/// `GO WARP` 3, the event log alternating `NEXT start/done, BACK start/done` every hold. The pair
/// was split across two sub-states of one turn, so 12.9's rule held and the loop survived it:
/// `NEXT` on the top-level menu was an A press on FIGHT, which **opened** the move list, and `BACK`
/// on the move list **closed** it again. Section 12.10 takes `NEXT` off every pad with a cursor
/// accepting input and gives the bag its own; `MOVE 1` is the backstop the top-level menu keeps.
///
/// The claim is about the *turn*, not about the fight: that a battle from this state ends, that it
/// ends on a bounded number of macros, that the fly's own presses are what ends it, and that none
/// of the three traps is on the pad any more. Which move it picks and whether it wins are the
/// fly's.
///
/// ```sh
/// FLY_ROM=/path/to/pokemon-red.gb \
/// FLY_FOREST_CHECKPOINT=.local/checkpoints/release-forest-rung9.checkpoint \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture
/// ```
#[test]
fn the_battles_turns_advance_from_the_rung_nine_forest_checkpoint() {
let rom = skip_without_rom!();
let Some(checkpoint) = forest_checkpoint() else {
eprintln!("skipped: no FLY_FOREST_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
let from = run.map();
assert!(
from == VIRIDIAN_FOREST || from == ROUTE_2 || from == VIRIDIAN_FOREST_SOUTH_GATE,
"the checkpoint is the one the stream stalled on, got map {from:#04x}"
);
// Rung 9 is stood on, so the objective is rung 10 — Pewter City — and the road there is north
// through the forest (`macros::geography`, asserted as hops in that module's own tests).
assert_eq!(
run.objective_map(),
Some(0x02),
"the objective is Pewter City, whatever errand is in front of it"
);
// Every battle this run passes through, and how it left.
let (mut battles, mut ended, mut in_battle) = (0u32, 0u32, run.in_battle() != 0);
let mut own_turns = 0u32;
for _ in 0..240_000 {
run.frame();
let now = run.in_battle() != 0;
match (in_battle, now) {
(false, true) => battles += 1,
(true, false) => ended += 1,
_ => {}
}
in_battle = now;
if now && run.own_turn() {
own_turns += 1;
}
if run.route.contains(&VIRIDIAN_FOREST_NORTH_GATE) {
break;
}
}
let counts = run.layer.counts();
eprintln!(
"from map {from:#04x} in {:.1} brain minutes: route {:?}, battles {battles} ({ended} ended), own-turn frames {own_turns}, macros {counts:?}, by name {:?}, objective on the pad on {:?}",
run.ms / 60_000.0,
run.route,
run.started,
run.objective_on_pad
);
// The traps, as assertions on the cartridge.
eprintln!(
"`BACK` in a battle was dealt on {:?}; `NEXT` on {:?}",
run.battle_back_where, run.battle_next_where
);
assert!(
!run.back_without_a_list,
"`BACK` was on a battle pad with no list open: {:?}",
run.battle_back_where
);
assert!(!run.threw_at_a_held_species, "a ball was thrown at a species the party holds");
eprintln!("blocked where: {:?}", run.blocked_where);
// Section 12.11, the other half of `THROW BALL`: it reaches the bag. On v0.4.3 every start of
// it was `blocked` -- 63 of 63, mean sixty-nine frames -- because the step that walks the bag
// list was allowed to read the battle menu's cursor while the bag was still drawing.
assert_eq!(
run.blocked.get("THROW BALL"),
None,
"`THROW BALL` reported blocked: {:?}",
run.blocked
);
// Section 12.10, the two halves of it.
assert!(
!run.next_and_back_on_one_pad,
"a battle pad held both `NEXT` and `BACK`: two buttons that undo each other"
);
assert!(
!run.next_on_a_menu_accepting_input,
"`NEXT` was on the pad while a battle menu was accepting input, where A opens rather \
than advances"
);
// And the shape the log had, rather than only the pads it came from. Two in a row is the
// rotation happening to deal the pair; the live run did it for seventy-one hours.
assert!(
run.longest_next_back_alternation < 4,
"`NEXT`/`BACK` alternated {} times in a row",
run.longest_next_back_alternation
);
// The turn moves: the fly's own battle presses happen, and a battle this run entered or
// resumed finishes.
assert!(own_turns > 0, "the fly never got a turn");
let battle_presses: u32 = run
.started
.iter()
.filter(|(name, _)| name.starts_with("MOVE ") || **name == "THROW BALL")
.map(|(_, n)| *n)
.sum();
assert!(battle_presses > 0, "no move and no ball: {:?}", run.started);
assert!(ended > 0, "no battle ever ended: {battles} entered");
// **Every battle that started, finished**, and each one on a bounded number of macros. That is
// the claim the 2-cycle breaks, and the way it breaks it is the opposite of a slow fight: the
// battle never leaves the fly's own turn at all, so the count grows with the *run*. On v0.4.2
// from this same checkpoint the trap hunt spent all twenty of its brain minutes -- 71,673
// frames, 1,489 macros, 73 of 73 windows flagged -- inside **one** battle that never ended,
// with `BACK` 739 starts on the move list and `NEXT` 739 on the top-level menu.
//
// The bound is **seven hundred** and was four; the number it is generous against is **450**
// and was 275, re-measured after section 12.11 put `battle_entry`'s `ITEM` and `PKMN` the
// right way round. Before that `SWITCH` opened the bag and `ITEM` and `THROW BALL` opened the
// party list, so neither could ever finish: every turn was an attack or nothing. A fly that
// can switch and heal fights longer, which is a longer battle and not a stalled one -- what
// the assertion is for is the difference between a battle that ends and a cycle that does
// not, and 3 of 3 ended here.
assert!(
run.battles_ended + 1 >= run.battles_entered,
"{} battles entered and only {} left: a battle was entered and never got out",
run.battles_entered,
run.battles_ended
);
assert!(
run.worst_battle_macros > 0 && run.worst_battle_macros < 700,
"the worst battle cost {} macros over {} that ended",
run.worst_battle_macros,
run.battles_ended
);
eprintln!(
"battles: {} entered, {} ended, worst {} macros; longest NEXT/BACK alternation {}",
run.battles_entered,
run.battles_ended,
run.worst_battle_macros,
run.longest_next_back_alternation
);
// `BACK` is still pressed, and that is the contract rather than a residual: over the move list
// and over a one-Pokemon party list it is one of the two answers a list has, and where it
// leads is a menu with the move buttons on it (row 34). Its share is *reported* -- under this
// harness's game-blind rotation it is a fact about the rotation and not about the macros,
// because every bound channel wins about equally often.
let backs = run.started.get("BACK").copied().unwrap_or(0);
eprintln!(
"`BACK` was {backs} of {} macro starts, none of them with no list open",
counts.started
);
// North is the road, and the gate is the first hop. Asserted when the run reaches it and
// reported when it does not: which way the fly walks is its own, and the objective being on
// the pad is what this harness can hold it to.
if run.route.contains(&VIRIDIAN_FOREST_NORTH_GATE) {
eprintln!("the fly left the forest north through the gate at 0x2f");
} else {
eprintln!(
"the forest's north gate was not reached in this run; route {:?}",
run.route
);
}
assert!(
run.objective_on_pad.contains(&VIRIDIAN_FOREST)
|| run.objective_on_pad.contains(&ROUTE_2)
|| run.objective_on_pad.contains(&VIRIDIAN_FOREST_NORTH_GATE),
"GO OBJECTIVE was on no overworld pad on the road north: {:?}",
run.objective_on_pad
);
}
/// From the stalled checkpoint, nothing the map draws is read as a text box.
///
/// **What this was written to settle** (2026-09-17, `infra/docs/macros-traps.md`): with the walk
/// fixes in, 41,012 frames of a 71,673-frame run read as `Scene::Dialog`, and two readings fitted
/// that — a real text box the pad cannot advance, or a `dialog` reading off map tiles that merely
/// look like a box. The dialog branch has two halves, `wFontLoaded`'s bit and the drawn border, so
/// the way to tell is to count the frames on which they disagree.
///
/// The answer was that the detector is right and the corner test was thin. Over 43,004 frames of
/// the real brain driving from this checkpoint: **0** frames had the font flag set with the four
/// corners drawn and the rest of the border missing — every `dialog` frame was a fully drawn
/// `TextBoxBorder` — while **315** frames had all four corners drawn with the font flag clear,
/// because the frame's tile ids ($79, $7b, $7d, $7e) are ordinary ground in the overworld
/// tilesets. So the flag was the only thing keeping a map out of the scene, and `waiting` now
/// reads the whole border instead of its corners.
///
/// This test holds the first half of that on the cartridge. The count of the second is printed
/// rather than asserted: it is a fact about which tiles the fly happens to walk past, and a run
/// that never sees one has not proved anything either way.
#[test]
fn the_map_is_never_read_as_a_text_box_from_the_stalled_checkpoint() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
let (mut real, mut fooled, mut no_corners, mut corners_only) = (0u32, 0u32, 0u32, 0u32);
for _ in 0..60_000 {
run.frame();
let text = flybrain_gb::pokemon_red::state::text_box(&mut run.gb);
let (corners, border) = flybrain_gb::pokemon_red::state::dialog_border(&mut run.gb);
match (text.open, corners, border) {
(true, true, true) => real += 1,
(true, true, false) => fooled += 1,
(true, false, _) => no_corners += 1,
(false, true, _) => corners_only += 1,
(false, false, _) => {}
}
}
eprintln!(
"dialog branch over 60,000 frames: real box {real}, fooled {fooled}, \
font without corners {no_corners}, corners without the font {corners_only}"
);
assert_eq!(
fooled, 0,
"a frame the detector would call `dialog` on four map tiles: the border was not drawn"
);
// `waiting` is never true without the WRAM flag, whatever the map is drawing.
assert!(
!flybrain_gb::pokemon_red::state::text_box(&mut run.gb).waiting
|| flybrain_gb::pokemon_red::state::text_box(&mut run.gb).open
);
}
/// From the stalled checkpoint, the objective is Oak, and the fly walks to him and presses A.
///
/// **Row 29** (`infra/docs/macros-traps.md`). With the objective pointing at Oak's *lab*, a place
/// that is only a map, `GO OBJECTIVE` arrived on the lab's own doormat and called that arriving;
/// `GO OUT` walked straight back out; the pair ran every three brain seconds at the door, live, for
/// most of a morning. A rung earned by a conversation is not a map — it is somebody to stand next
/// to — so the catalog names the *person*, `GO OBJECTIVE` arrives beside him facing him, and `TALK`
/// is on the pad on the next hold. While that person is on this map, untalked and unexcluded, the
/// ways out are not on the pad at all: there is nothing for the bounce to bounce on.
///
/// This is the chain on the cartridge with the game-blind rotation driving it: the parcel carried
/// and undelivered, the lab reached by way of Pallet Town rather than through the shut road, and
/// `TALK` taken in the room the rung is in. The delivery event itself is measured rather than
/// asserted; the comment at that assertion has the numbers and the reason.
#[test]
fn macros_mode_reaches_the_errand_and_takes_its_press_from_the_stalled_checkpoint() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
assert_eq!(run.map(), VIRIDIAN_CITY, "the checkpoint is the one the stream stalled on");
assert!(run.event(EVENT_GOT_OAKS_PARCEL), "the parcel is carried");
assert!(!run.event(EVENT_OAK_GOT_PARCEL), "and undelivered, which is what shuts the road");
run.drive_until("oak's lab", 120_000, |run| run.map() == OAKS_LAB);
// **The delivery itself is no longer asserted, and that is a measurement rather than a trim.**
//
// It was, when this test was written: `EVENT_OAK_GOT_PARCEL` at 27,167 frames on 504 macros.
// That held for exactly one trajectory. The battle sub-states of section 12.6 changed what the
// fly does in Route 1's grass -- it now chooses a move instead of pressing A at the text -- and
// the rotation's phase inside the lab moved with it: from the same checkpoint the fly reaches
// the lab, presses `TALK` ninety-four times, and does not deliver in **900,000 frames** (16
// brain hours).
//
// Which is the sibling tests' own finding in a new place: a game-blind rotation does not aim,
// and the delivery is one A press at one particular person. What the *macros* can be held to is
// that the fly is sent at the errand, gets there, and has the press available -- and that is
// what this test holds. The delivery belongs to a run with a brain in it and a reward for the
// rung. The gap the trajectory exposed is named in `infra/docs/macros-traps.md` row 31.
run.drive_until("a talk in the lab", 240_000, |run| {
run.map() == OAKS_LAB && run.started.get("TALK").is_some_and(|count| *count > 0)
});
assert_eq!(run.map(), OAKS_LAB, "in the room the rung is in");
assert!(run.started.get("TALK").is_some_and(|count| *count > 0), "{:?}", run.started);
let counts = run.layer.counts();
assert!(counts.done > counts.timeout, "the walks completed: {counts:?}");
assert!(counts.started < 3_000, "macros spent reaching the errand: {counts:?}");
eprintln!(
"the errand reached and pressed in {:.1} brain minutes on {} macros ({counts:?}), \
route {:?}, by name {:?}",
run.ms / 60_000.0,
counts.started,
run.route,
run.started
);
}
/// From the stalled checkpoint, the objective is the errand and the fly walks to it.
///
/// **The gate** (2026-09-17, `infra/docs/macros-traps.md` row 28). The fly stepped north out of
/// (18, 10) in Viridian City into a still sprite at (18, 9); the cartridge showed a box reading
/// "This is private property!", took the joypad and walked it back one tile. Every macro that hit
/// it ended `Done` — a scene change, which records nothing — so the same step was taken once per
/// hold for eight hours.
///
/// **What shut the road is two rungs the ladder had skipped.** The save has
/// `EVENT_GOT_OAKS_PARCEL` set and `EVENT_OAK_GOT_PARCEL` clear: the parcel is carried and
/// undelivered, so rungs 6 (the delivery) and 7 (the Pokédex) are unearned — while the rank reads
/// **8**, because the rank is the *maximum* over satisfied rungs and Viridian City had been stood
/// on. `objective` was `rank + 1`, which aimed the fly at Viridian Forest, north, through a gate
/// the cartridge keeps shut until Oak has his parcel. It is the lowest *unsatisfied* rung now,
/// which is Oak's lab, two maps south — and nothing in the macros knows any of that: the catalog
/// already held the place and the arithmetic was picking the wrong rung out of it.
///
/// So this asserts what the change makes true and what the cartridge can be held to: the fly
/// **leaves Viridian City and reaches Oak's lab**, where the errand is, on a bounded number of
/// macros.
///
/// **It takes the town's errands on the way, and that is section 13 working.** When this test was
/// written the lab was 1.4 brain minutes and 55 macros away; the mart and the Pokémon Center are
/// now ahead of the rung's place in `GO OBJECTIVE`, so the measured route is Viridian, the centre,
/// the mart, Route 1, Pallet Town, the lab — 13.5 brain minutes and 788 macros, of which 777
/// completed. The bound is what this test is for (the walks finish and the count is finite), not
/// the number itself.
///
/// **Route 2 is not asserted, and that is a measurement.** Delivering the parcel is one A press at
/// one particular person, and this readout is a rotation that knows nothing about the game: over
/// 600,000 frames — eleven brain hours — inside and around the lab it never pressed it, exactly as
/// the sibling test above records for the starter ("a game-blind stub does not press `TALK` in
/// front of a Pokéball on purpose"). That claim belongs to a run with a brain in it, and the road
/// north is behind it.
#[test]
fn the_objective_is_the_errand_and_macros_mode_walks_to_it() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
assert_eq!(run.map(), VIRIDIAN_CITY, "the checkpoint is the one the stream stalled on");
assert_eq!(run.adapter.progress().rank, 8, "rank 8, the maximum over the satisfied rungs");
assert!(run.event(EVENT_GOT_OAKS_PARCEL), "the parcel is carried");
assert!(!run.event(EVENT_OAK_GOT_PARCEL), "and undelivered, which is what shuts the road");
// South, not north: Route 1, Pallet Town, the lab.
run.drive_until("oak's lab", 120_000, |run| run.map() == OAKS_LAB);
assert!(run.route.contains(&PALLET_TOWN), "by way of the town: {:?}", run.route);
assert!(!run.route.contains(&ROUTE_2), "and not through the shut road: {:?}", run.route);
let counts = run.layer.counts();
assert!(counts.done > counts.timeout, "the walks completed: {counts:?}");
// Bounded rather than tight: the two errands of section 13 are ahead of the rung's place, so
// this is three buildings' worth of walking and not one town's.
assert!(counts.started < 1_500, "macros spent getting to the errand: {counts:?}");
eprintln!(
"the errand in {:.1} brain minutes on {} macros ({counts:?}), route {:?}, by name {:?}",
run.ms / 60_000.0,
counts.started,
run.route,
run.started
);
}
/// After a restore that added the macro roles, `TALK` wins a facing window.
///
/// **The live finding** (2026-09-17, after v0.3.6): forty-eight minutes in Oak's lab with `TALK`
/// never chosen once. The macro populations were firing — 27 to 145 Hz across the six roles — and
/// the decoder had no baseline for any of them, because `calibrate()` runs once at warm-up and the
/// live state was restored from a checkpoint written before the roles existed. An absent baseline
/// reads as zero, so the score `(rate + 1) / (baseline + 1)` becomes `rate + 1` and the group is
/// decided by raw rate: `macro_talk` at 41 Hz cannot beat `macro_frontier` at 54 in the one window
/// where `TALK` is on the pad at all.
///
/// So this drives the shipping loop from the checkpoint with a decoder state of exactly that shape
/// — calibrated, then every macro baseline removed, which is what the box restored — and counts
/// **facing windows**: runs of frames in which `TALK`'s channel is on the pad, the only chances
/// the fly gets. `TALK` is expected to win one of the first few, not one of the first hundred.
///
/// The stub's own rates are not the live ones and cannot be; what carries over is the *state shape*
/// and the rule that answers it. The live numbers are pinned in the unit tests and the `restore`
/// golden.
#[test]
fn talk_wins_a_facing_window_after_a_restore_that_added_the_macro_roles() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume_with(&rom, MacroMode::Macros, &checkpoint, true);
run.drive_until("a chosen TALK", 240_000, |run| {
run.started.get("TALK").is_some_and(|count| *count > 0)
});
assert!(
run.talk_windows > 0 && run.talk_windows <= 32,
"TALK won a facing window within a bounded number of them: {} windows, {:?}",
run.talk_windows,
run.started
);
eprintln!(
"TALK chosen after {} facing windows, {:.1} brain minutes, {:?}",
run.talk_windows,
run.ms / 60_000.0,
run.started
);
}
/// With the move list open on the cartridge, macros mode chooses a move and the turn proceeds.
///
/// **The live loop** (2026-09-17, rank 9, Viridian Forest, an hour and forty-one minutes): a wild
/// Kakuna, Bulbasaur at 8/28, the FIGHT list open with TACKLE showing 0 of 40 PP — and the pad was
/// one `NEXT`, every 268 brain milliseconds. `own_turn` was the top-level menu alone, so a frame
/// with the move list open read as *between turns*, whose pad is an A press on whatever the cursor
/// is sitting on. It pressed A at the move with no PP; the game said so; the box closed; the list
/// came back.
///
/// This drives the shipping loop from the checkpoint until a wild battle starts, presses A to open
/// the move list, and holds the three things that were wrong:
///
/// - the frame reads as the fly's turn, not as between turns;
/// - the pad over the open list is `ATTACK` and `BACK`, and never `NEXT`;
/// - `ATTACK` confirms a move that **has** PP, and the list closes: the turn proceeds.
///
/// The 0-PP-first-move case itself is pinned synthetically, in
/// `attack_over_an_open_move_list_chooses_a_move_that_has_pp` and
/// `attack_confirms_anyway_when_every_move_is_out_of_pp` — a fake can set a PP counter to zero and
/// a cartridge cannot be talked into it inside a test.
#[test]
fn macros_mode_chooses_a_move_from_an_open_list_on_the_cartridge() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
// A wild battle, by the fly's own walking: Route 1's grass is on the way to the errand.
run.drive_until("a wild battle", 240_000, |run| {
run.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wIsInBattle) != 0
});
// Advance the battle's opening text until the top-level menu is up, then open FIGHT. Raw
// presses rather than macros, because what is under test is the pad the *list* deals.
let mut opened = false;
for _ in 0..600 {
let menu = {
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(&mut run.gb);
flybrain_gb::pokemon_red::macros::palette::battle_menu(&mut state)
};
// A *settled* list: one whose cursor the seam can place. The coordinates appear on the
// battle's opening frames before the engine has filled `wBattleMon*`, and that frame is
// not a choice the game is waiting for (`pokemon_red::state::battle`).
if matches!(
menu,
flybrain_gb::pokemon_red::macros::state::BattleMenu::Moves { cursor: Some(_), .. }
) {
opened = true;
break;
}
for phase in 0..16 {
run.gb.set_buttons(if phase < 8 { flybrain_gb::buttons::A } else { 0 });
run.gb.run_frame().expect("a frame should complete");
run.ms += MS_PER_FRAME;
}
let ms = run.ms;
run.adapter.sample(&mut run.gb, ms);
}
assert!(opened, "the move list never opened");
// What the scene and the pad say about it.
let scene = {
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(&mut run.gb);
let state: &mut dyn flybrain_gb::pokemon_red::macros::cartridge::MacroState = &mut state;
state.scene()
};
assert!(
matches!(
scene,
flybrain_gb::pokemon_red::macros::state::Scene::Battle { own_turn: true, .. }
),
"the move list open is the fly's turn: {scene:?}"
);
let ledger = AdapterLedger(&run.adapter);
let _ = run.layer.observe(&mut run.gb, &ledger, run.ms);
let pad: Vec<String> =
run.layer.feed_palette().into_iter().map(|slot| slot.name).collect();
assert!(pad.iter().any(|name| name.starts_with("MOVE ")), "the list is chosen from: {pad:?}");
assert!(pad.iter().any(|name| name == "BACK"), "or backed out of: {pad:?}");
assert!(
!pad.iter().any(|name| name == "NEXT"),
"never an A press on whatever the cursor holds: {pad:?}"
);
// And `ATTACK` confirms a move that has PP, which closes the list: the turn proceeds.
let slot = {
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(&mut run.gb);
flybrain_gb::pokemon_red::macros::palette::move_list(&mut state).and_then(|(cursor, _)| cursor)
}
.expect("a move to confirm");
let pp_before = run.gb.read_wram(
flybrain_gb::pokemon_red::symbols::ram::wBattleMonPP + u16::from(slot),
);
assert!(pp_before > 0, "the chosen move has PP: slot {slot} at {pp_before}");
// Lean on ATTACK's own channel so the script under test is the one that runs, from a clean
// decision: whatever the rotation was holding when the list opened is cancelled first, because
// a `BACK` already in flight would close the list before `ATTACK` could be offered.
let ms = run.ms;
let _ = run.layer.cancel(ms);
run.decoder.clear_holds(ms);
run.force_hot = Some("macro_move_1");
run.drive_until("a MOVE chosen", 6_000, |run| {
run.started.keys().any(|name| name.starts_with("MOVE "))
});
run.drive_until("the list closed", 6_000, |run| {
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(&mut run.gb);
!matches!(
flybrain_gb::pokemon_red::macros::palette::battle_menu(&mut state),
flybrain_gb::pokemon_red::macros::state::BattleMenu::Moves { .. }
)
});
eprintln!(
"the move list closed on slot {slot} (PP was {pp_before}), macros {:?}, by name {:?}",
run.layer.counts(),
run.started
);
assert!(
run.started.keys().any(|name| name.starts_with("MOVE ")),
"a MOVE button is what closed it: {:?}",
run.started
);
}
/// From the gate-house stall's own checkpoint, macros mode leaves northward into the forest.
///
/// **The stall** (2026-09-17 17:32 UTC, `infra/docs/macros-traps.md` rows 32 and 33): rank 9
/// (Viridian Forest, next Pewter City) for four hours and eighteen minutes with the fly on map 50,
/// the ten-by-eight gate house between Route 2 and Viridian Forest. The pad was `GO FRONTIER` and
/// nothing else, and the last 40 KB of the event log was 175 `GO FRONTIER start`/`done` pairs —
/// one tile of walking each, one every 800 brain milliseconds. Two facts made it:
///
/// - the gate's two doormats are walkable ground the *reward* ledger can never record, because
/// `sample`'s payout gate rejects `wMovementFlags`' door and warp bits, so they were a frontier
/// nothing could retire and `GO FRONTIER` never exhausted;
/// - `geography::next_hop` had one node for `ROUTE_2`, whose two halves are not connected to each
/// other, so "the first hop to Pewter" from inside the gate was Route 2 *south* — the door the
/// fly had just come in by. `GO OBJECTIVE` aimed back out of it, `GO OUT` had no other tier, and
/// `GO WARP` — the north doors into the forest, which are the way to Pewter — was never on the
/// pad at all.
///
/// **What is asserted is the direction.** The first map the fly leaves the gate house for is
/// Viridian Forest, which is northward and toward the rung; before this branch it was Route 2,
/// southward, and the fly came straight back. And the gate house never deals a pad of one button
/// again, which is the loop's own signature. Both on a bounded number of macros: the measured run
/// leaves on **one macro in 155 frames**.
///
/// **The forest's north gate is not asserted, and that is a measurement rather than a trim.** With
/// this branch the fly crosses into the forest at once and plays it — 1,200,000 frames (5.6 brain
/// hours) from this checkpoint cover six maps and 20,595 macros, including a blackout to Pallet
/// Town and the walk back north — and it does not reach map 47. From about 600,000 frames on the
/// only macros that start are `ITEM`, `BACK` and `NEXT`: `ATTACK` does not start once more and
/// every overworld count freezes, which is a battle whose own turn the fly never ends. That is the
/// next trap, recorded as row 34 in `infra/docs/macros-traps.md`, and asserting Route 2's north
/// half here would be a test that fails on a trap it does not measure.
///
/// Gated on `FLY_ROM` and on a checkpoint whose map is the gate house, and skips cleanly without
/// either:
///
/// ```sh
/// FLY_ROM="$HOME/fly-plays-pokemon/Pokemon Red (U) [S][BF].gb" \
/// FLY_GATE_CHECKPOINT=.local/checkpoints/release-rank9-20260917T1732.checkpoint \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture
/// ```
#[test]
fn macros_mode_leaves_the_forests_south_gate_northward() {
let rom = skip_without_rom!();
let Some(checkpoint) = gate_checkpoint() else {
eprintln!("skipped: no FLY_GATE_CHECKPOINT / FLY_TRAP_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
if run.map() != VIRIDIAN_FOREST_SOUTH_GATE {
eprintln!(
"skipped: the checkpoint is on map {:#04x}, not the forest's south gate",
run.map()
);
return;
}
// Off the map the stall was on. Bounded at 24,000 frames, which is six brain minutes; the
// measured run needs 155 of them, because the north doors are seven tiles from the
// checkpoint's own tile and one walk covers that.
run.drive_until("off the gate house", 24_000, |run| {
run.route.iter().any(|map| *map != VIRIDIAN_FOREST_SOUTH_GATE)
});
assert_eq!(
run.route.first().copied(),
Some(VIRIDIAN_FOREST_SOUTH_GATE),
"route {:?}",
run.route
);
assert_eq!(
run.route.get(1).copied(),
Some(VIRIDIAN_FOREST),
"the first map out of the gate house is the forest, northward and toward the rung, not Route 2 behind it: route {:?}, by name {:?}",
run.route,
run.started
);
let counts = run.layer.counts();
assert!(counts.started <= 5, "macros spent leaving the gate house: {counts:?}");
// Then keep going, because the pad claim is about what the gate house deals *after* its
// frontier has been walked and its people talked to — which is the state four hours of stream
// was in, and which the session ledgers only reach by running.
for _ in 0..120_000 {
run.frame();
}
let gate_pad = run.pads.get(&VIRIDIAN_FOREST_SOUTH_GATE).copied().unwrap_or(0);
assert!(
gate_pad > 1,
"the gate house dealt a pad of {gate_pad} buttons: {:?}, by name {:?}",
run.pads,
run.started
);
let counts = run.layer.counts();
assert!(counts.done > 0, "no macro completed: {counts:?}");
eprintln!(
"out of the gate house northward on {} macros, smallest gate-house pad {gate_pad}, {:.1} brain minutes, route {:?}, {counts:?}, by name {:?}",
run.started.values().sum::<u32>(),
run.ms / 60_000.0,
run.route,
run.started
);
}
/// The checkpoint the gate-house test starts from, or `None` to skip.
///
/// Its own variable before `FLY_TRAP_CHECKPOINT`, because the two stalls on this rung have
/// different checkpoints and each test asserts the map its own stall was on. The test skips rather
/// than fails when the checkpoint it gets is the other one.
fn gate_checkpoint() -> Option<flysim::store::Checkpoint> {
let path = std::env::var_os("FLY_GATE_CHECKPOINT")
.or_else(|| std::env::var_os("FLY_TRAP_CHECKPOINT"))
.or_else(|| std::env::var_os("FLY_MACRO_CHECKPOINT"))?;
Some(
flysim::store::load(std::path::Path::new(&path))
.expect("the checkpoint should be a FLYSIM01 envelope"),
)
}
/// From the 0-PP battle's own checkpoint, the fly's own turn has a button that ends it.
///
/// **The stall** (`infra/docs/macros-traps.md` row 34, measured in Viridian Forest at 99.1 brain
/// minutes): a wild battle, a party of one, TACKLE / GROWL / LEECH SEED all at 0 PP, a potion in
/// the bag and a `RUN` the cartridge refuses. `ATTACK`'s precondition was `best_move(..).is_some()`
/// and `best_move` skips a move with no PP, so the button left the top-level menu — and the move
/// list, where `move_list_choice` confirms a slot anyway so that Struggle happens, is only ever
/// reached *through* FIGHT. What was left was `ITEM` over a bag it could only close and `NEXT` on
/// whatever the cursor held, which opens the party list, whose only bound button with one Pokémon
/// is `BACK`. 600,000 frames of it.
///
/// What is asserted: `ATTACK` is on the pad of the fly's own turn, the macro behind it **starts
/// and runs on the cartridge** from this state, that turn never deals one button, and the battle
/// ends — which is the whole claim, because a turn that can be ended is a battle that finishes one
/// way or the other. Which way is not asserted: with 6 of 34 HP against a trainer the measured run
/// faints and blacks out, and that is the game rather than the macro layer.
///
/// Gated on `FLY_ROM` and on a checkpoint in that state, which
/// `examples/scene_probe.rs` writes with `FLY_PROBE_CATCH=noattack FLY_PROBE_SAVE=…` — 99 brain
/// minutes of the stub from the gate-house checkpoint, the release box's own run carried forward.
/// `.local/` is not tracked, so this skips cleanly without it:
///
/// ```sh
/// FLY_ROM="$HOME/fly-plays-pokemon/Pokemon Red (U) [S][BF].gb" \
/// FLY_NOPP_CHECKPOINT=.local/checkpoints/release-nopp.checkpoint \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture
/// ```
#[test]
fn macros_mode_ends_a_turn_with_no_move_left_on_the_cartridge() {
let rom = skip_without_rom!();
let Some(checkpoint) = nopp_checkpoint() else {
eprintln!("skipped: no FLY_NOPP_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
if run.in_battle() == 0 {
eprintln!("skipped: the checkpoint is not in a battle");
return;
}
assert_eq!(run.party(), 1, "a party of one is half of what made the turn unanswerable");
// First the script, with the attack channel leaned on rather than rotated to. That is what
// `force_hot` is for in this harness -- the one question that is about a *script* rather than
// about which button the fly picks -- because a rotation may spend the whole turn on `NEXT`
// and prove nothing about `ATTACK` either way.
run.force_hot = Some("macro_move_1");
run.drive_until("a `MOVE` to start on this turn", 12_000, |run| {
run.started.keys().any(|name| name.starts_with("MOVE "))
});
run.force_hot = None;
// Then the battle ends. Bounded at 60,000 frames, which is 17 brain minutes; the measured run
// needs 1,536.
run.drive_until("the battle to end", 60_000, |run| run.in_battle() == 0);
assert!(
run.move_button_on_battle_pad,
"no `MOVE` button was ever on the pad of the fly's own turn: {:?}, by name {:?}",
run.battle_pad,
run.started
);
assert!(
run.battle_pad.is_some_and(|dealt| dealt > 1),
"the fly's own turn dealt a pad of {:?} buttons: by name {:?}",
run.battle_pad,
run.started
);
eprintln!(
"the turn ended in {:.1} brain minutes, smallest battle pad {:?}, {:?}, by name {:?}",
run.ms / 60_000.0,
run.battle_pad,
run.layer.counts(),
run.started
);
}
/// The checkpoint the no-PP test starts from, or `None` to skip.
///
/// Its own variable and no fallback: the other checkpoints in this file are overworld states and
/// this test asserts a battle, so taking one of them would fail on a state it does not measure.
fn nopp_checkpoint() -> Option<flysim::store::Checkpoint> {
let path = std::env::var_os("FLY_NOPP_CHECKPOINT")?;
Some(
flysim::store::load(std::path::Path::new(&path))
.expect("the checkpoint should be a FLYSIM01 envelope"),
)
}
// -------------------------------------------------------------------------------------------
// Section 13: the errands, a purchase and a heal, from the release box's own checkpoint
// -------------------------------------------------------------------------------------------
/// `GO SHOP` walks the fly into the Viridian mart, once, and then to its counter.
///
/// **What is proven here, and what is not.** The errand works: from the release container's own
/// Viridian checkpoint the fly is inside the mart in 1.4 brain minutes, the errand is discharged
/// on arrival and `GO SHOP` is never on the city's pad again. Inside, `GO SHOP` walks it to a tile
/// it can talk to the clerk from — which needs the counter reach, because every tile beside a
/// clerk behind a desk is a wall.
///
/// **The purchase is not ROM-proven from this checkpoint**, and the mechanism is named rather than
/// papered over (`infra/docs/macros-traps.md` rows 33b and 34b). Two things are in the way, both
/// measured on the cartridge on 2026-09-17:
///
/// - the screen-buffer tile read disagrees with itself on a map smaller than the screen. In the
/// Viridian mart the same tile reads walkable-and-counter from one of the fly's tiles and
/// wall-and-not-counter from another two tiles away, because an 8x8 map cannot centre under a
/// ten-by-nine view. `facing_target`'s reach is taken from the *map id* for exactly that reason,
/// but the walk's own goals are still priced off the tile read;
/// - a counter *faced* is lost to the next hold. `GO SHOP` ends standing at the counter looking at
/// the clerk, and `TALK` is on the pad there — but a hold is 800 ms and whichever macro wins the
/// next one turns the fly away before the A press happens. The `reached` window brings `GO SHOP`
/// back in ten brain minutes, so it is bounded rather than a loop; it is not a proof.
///
/// The purchase scripts themselves are covered by the fake-game tests
/// (`a_purchase_navigates_by_the_items_place_in_the_stock_list`), and the *stock* read is proven
/// against the cartridge below: Viridian's counter sells no Potion.
#[test]
fn go_shop_enters_the_mart_once_and_walks_to_its_counter() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
assert_eq!(run.map(), VIRIDIAN_CITY, "the checkpoint is the one the stream stalled on");
// The errand is outstanding at the checkpoint: the ledger is session state and a restore
// starts empty (`docs/design/macros.md` section 13).
run.frame();
assert!(
run.layer.bound_channels().iter().any(|channel| channel == "macro_go_shop"),
"`GO SHOP` is on the pad with the errand outstanding: {:?}",
run.layer.bound_channels()
);
// Into the mart, on `GO SHOP`'s own channel. Viridian is a wide city and the mart is on the
// far side of it, so this is several budgets' worth of walking and several holds.
run.force_hot = Some("macro_go_shop");
let mart = u32::from(flybrain_gb::pokemon_red::maps::VIRIDIAN_MART);
run.drive_until("inside the mart", 240_000, |run| run.map() == mart);
// Inside, `GO SHOP` is still the walk to the clerk -- the half of section 13 the ledger does
// not gate, because the errand's remaining job is to put the fly where the purchases can be
// pressed. Every tile beside the clerk is a wall, so reaching one at all is the counter reach
// doing the work (`docs/design/macros-wram.md` section 7).
let mut shop_seen = false;
let mut faced_the_counter = false;
let mut pad_in_the_mart: Vec<String> = Vec::new();
run.drive_until("the counter faced", 60_000, |run| {
if run.map() != mart {
return false;
}
let bound = run.layer.bound_channels();
let shop = bound.iter().any(|channel| channel == "macro_go_shop");
// The frames straight after a warp report the new map with the *old* coordinates -- the
// header changes before the position does -- so the pad on those is dealt against a tile
// of the city. Waiting for `GO SHOP` to appear is waiting for the fly to actually be in
// the building.
if !shop_seen {
shop_seen = shop;
if shop_seen {
pad_in_the_mart = bound.clone();
}
return false;
}
// `GO SHOP` leaving the pad inside the building *is* the counter having been faced: the
// reached ledger is what retires it (`palette::counter_aims`).
faced_the_counter |= !shop;
faced_the_counter
});
eprintln!(
"the mart's pad on arrival: {pad_in_the_mart:?}, counter stocks {:?}, wallet {}",
run.counter_stock(),
run.money()
);
assert!(faced_the_counter, "`GO SHOP` walked to the clerk and retired it");
// And while the counter was unfaced, nothing on the pad could leave the building: that is what
// stops the one visit this area gets being spent on the doormat (`palette::counter_pending`).
assert!(
!pad_in_the_mart.iter().any(|channel| channel == "macro_go_out"),
"no way out while the errand's counter is unfaced: {pad_in_the_mart:?}"
);
// Out again -- the suppression is released now -- and the errand is paid: `GO SHOP` is not on
// the city's pad a second time. That is "once per area per run", and it is what makes the
// errand impossible to loop on.
run.force_hot = Some("macro_go_out");
run.drive_until("back out onto the city", 120_000, |run| run.map() == VIRIDIAN_CITY);
run.force_hot = None;
for _ in 0..600 {
run.frame();
}
assert!(
!run.layer.bound_channels().iter().any(|channel| channel == "macro_go_shop"),
"the errand is paid and never offered again: {:?}",
run.layer.bound_channels()
);
}
/// `GO HEAL` walks the fly into the Viridian Pokémon Center and `HEAL` restores the party.
///
/// The nurse is `object_event 3, 1, SPRITE_NURSE` behind the counter tile at (3, 2), so none of
/// the four tiles around her can be stood on: the walk aims at (3, 3) and faces up, over the
/// counter, which is the only place the cartridge lets her be spoken to from
/// (`docs/design/macros-wram.md` section 7).
///
/// `HEAL` is on the pad only while the party is hurt or statused, so the test hurts it first —
/// by *reading* whether it already is, and skipping the assertion if the checkpoint's party is
/// already full, because this crate never writes game memory (`docs/loop-review.md`).
#[test]
fn go_heal_enters_the_centre_and_heal_restores_the_party() {
let rom = skip_without_rom!();
let Some(checkpoint) = checkpoint() else {
eprintln!("skipped: no FLY_TRAP_CHECKPOINT / FLY_MACRO_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
assert_eq!(run.map(), VIRIDIAN_CITY);
run.frame();
assert!(
run.layer.bound_channels().iter().any(|channel| channel == "macro_go_heal"),
"`GO HEAL` is on the pad with the errand outstanding: {:?}",
run.layer.bound_channels()
);
let centre = u32::from(flybrain_gb::pokemon_red::maps::VIRIDIAN_POKECENTER);
// The pad is captured *from inside the walk*, on the frames the fly is actually standing in
// the building. Measured 2026-09-17: given six hundred free frames after arriving it walks
// straight back out, and the centre's own row is only dealt while it is in there -- which is
// the mistake this test made on its first run.
let mut heal_seen = false;
let mut hurt_in_centre = false;
let mut rested_in_centre = false;
run.force_hot = Some("macro_go_heal");
run.drive_until("the centre's own pad", 240_000, |run| {
if run.map() != centre {
return false;
}
heal_seen |= run.layer.bound_channels().iter().any(|channel| channel == "macro_heal");
if run.party_rested() {
rested_in_centre = true;
} else {
hurt_in_centre = true;
}
heal_seen || rested_in_centre
});
run.force_hot = None;
eprintln!(
"in the centre: HEAL seen {heal_seen}, party hurt {hurt_in_centre}, rested \
{rested_in_centre}"
);
if !hurt_in_centre {
eprintln!(
"the checkpoint's party is already full and this crate never writes game memory, \
so `HEAL`'s restore is not asserted from this state; the walk to the counter is"
);
// What *is* asserted then: the button was never on the pad, which is its own
// precondition working (section 13: "only when at least one party member is not at full
// HP or has a status").
assert!(!heal_seen, "a rested party is no reason to rest");
return;
}
assert!(heal_seen, "`HEAL` was never on the centre's pad with a hurt party");
run.force_hot = Some("macro_heal");
run.drive_until("the party reads back full", 120_000, |run| run.party_rested());
run.force_hot = None;
assert!(run.party_rested(), "the nurse healed the party");
}
/// The rung-10 Pewter checkpoint, or `None` to skip.
///
/// Its own variable, like `FLY_FOREST_CHECKPOINT`: the tests above assert the map their envelope
/// is on and one envelope cannot be two maps.
fn building_checkpoint() -> Option<flysim::store::Checkpoint> {
std::env::var_os("FLY_BUILDING_CHECKPOINT").map(|path| {
flysim::store::load(std::path::Path::new(&path))
.expect("the checkpoint should be a FLYSIM01 envelope")
})
}
/// From the rung-10 checkpoint: the fly leaves the building, and no pad is `MENU` and a way back.
///
/// **What was live** (2026-09-22, thirty-one minutes after v0.4.3 deployed): rank 10, PEWTER CITY,
/// the fly on **map 0x35 -- the upper floor of the Pewter museum**, fourteen blocks by eight, one
/// warp at (7, 7) down to the floor below, two signs and three exhibits. For thirty brain minutes
/// the macro starts were `MENU` 82, `BACK` 82 and `GO FRONTIER` 8, the event log alternating `MENU
/// start/done, BACK start/done`.
///
/// Every candidate list on that map empties: `geography` has no row for the museum, so `next_hop`
/// answers `None` and `GO OBJECTIVE` has nothing to aim at (the objective itself is map 0x36 --
/// the gym leader, rung 11's BOULDER BADGE); the three exhibits and two signs are *reached* and
/// retired for the session; the four unstood tiles are walked or excluded; and the one way out is
/// a **passage** whose blocked window `unexcluded_exits` respects. That left `MENU`, which opens a
/// scene whose pad is `CLOSE`, `CONFIRM` and `BACK` -- and `BACK` closes it again.
///
/// The claims here are about the pad and about *leaving*, not about where the fly goes next:
///
/// - `MENU` is on **no** overworld pad, on any map the run stands on (section 12.11). On v0.4.3
/// this set holds every one of them, which is what makes this the regression test.
/// - no overworld pad is empty, which since 12.11 rests on the way out rather than on `MENU`.
/// - the fly **leaves map 0x35** on a bounded number of macros.
/// - `MENU`/`BACK` never alternate, and `THROW BALL` never reports `blocked` -- the two residuals
/// v0.4.3 left (63 starts, 63 blocked, mean sixty-nine frames).
///
/// ```sh
/// FLY_ROM=/path/to/pokemon-red.gb \
/// FLY_BUILDING_CHECKPOINT=.local/checkpoints/release-rank10-pewter.checkpoint \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture
/// ```
#[test]
fn the_fly_leaves_the_pewter_building_from_the_rung_ten_checkpoint() {
let rom = skip_without_rom!();
let Some(checkpoint) = building_checkpoint() else {
eprintln!("skipped: no FLY_BUILDING_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
let from = run.map();
assert_eq!(from, MUSEUM_2F, "the checkpoint is the building the stream stalled in");
// Rung 10 is stood on, so the objective is rung 11 -- the BOULDER BADGE, which is the gym
// leader and so a *person* on the gym's map.
assert_eq!(
run.objective_map(),
Some(0x36),
"the objective is the Pewter gym, where the badge is"
);
// Driven the whole budget rather than stopped at the door: leaving is one claim and "`MENU`
// is on no pad" is a claim about every pad the run deals, so the run keeps going and keeps
// recording. Thirty-three brain minutes, against the thirty the live run spent not leaving.
let mut left = None;
for frame in 0..120_000u32 {
run.frame();
if left.is_none() && run.map() != from {
left = Some(frame);
}
}
eprintln!(
"from map {from:#04x} in {:.1} brain minutes: route {:?}, macros {:?}, MENU on the pad \
of {:?}, empty pads {:?}, blocked {:?}",
run.ms / 60_000.0,
run.route,
run.started,
run.menu_on_pad,
run.empty_overworld_pads,
run.blocked
);
assert!(
run.menu_on_pad.is_empty(),
"`MENU` was on an overworld pad: {:?}",
run.menu_on_pad
);
assert_eq!(run.started.get("MENU"), None, "`MENU` cannot start if it is on no pad");
// One is a lone `BACK`, which is an ordinary press in a list; two is the pair, and the live
// run did it eighty-two times each for thirty brain minutes.
assert!(
run.longest_menu_back_alternation < 2,
"`MENU`/`BACK` alternated {} times in a row",
run.longest_menu_back_alternation
);
assert!(
run.empty_overworld_pads.is_empty(),
"an overworld pad was empty on {:?}",
run.empty_overworld_pads
);
let Some(left) = left else { panic!("the fly never left map {from:#04x}") };
eprintln!("it left map {from:#04x} on frame {left}");
assert!(
run.macros_on_the_first_map < 400,
"leaving the building cost {} macros",
run.macros_on_the_first_map
);
// The v0.4.3 residual, on the cartridge: every `THROW BALL` was blocked because the step read
// the battle menu's cursor while the bag was still drawing.
assert_eq!(
run.blocked.get("THROW BALL"),
None,
"`THROW BALL` reported blocked: {:?}",
run.blocked
);
}
/// The rung-10 Pokemon Center checkpoint, or `None` to skip.
fn center_checkpoint() -> Option<flysim::store::Checkpoint> {
std::env::var_os("FLY_CENTER_CHECKPOINT").map(|path| {
flysim::store::load(std::path::Path::new(&path))
.expect("the checkpoint should be a FLYSIM01 envelope")
})
}
/// From the rung-10 Pokemon Center checkpoint: the fly leaves the centre and stops answering YES.
///
/// **What was live** (2026-09-22, v0.4.4, rank 10 PEWTER CITY): the fly on **map 0x3a at (3, 3)**,
/// facing the nurse over her counter, and since the 09:39 restart the macro starts were `YES`
/// **2,142**, `TALK` 107, `GO FRONTIER` 26, `BACK` 24, the event log ending `YES start/done` for
/// ever. This is row 41, first measured in the rung-9 trap hunt and named as the next trap by
/// 12.11.
///
/// **What the survey found** (`infra/docs/macros-traps.md` row 41, and
/// `examples/scene_probe.rs`'s `FLY_PROBE_CATCH=nurse`): the nurse's conversation is a ring of
/// **forty-six A presses** -- welcome, the offer, the YES/NO box on **one** frame of the
/// forty-six, "OK. We'll need your POKeMON.", the machine, "fighting fit!", "We hope to see you
/// again!", the box closes for a single frame, and the next A press opens the whole thing again.
/// The party read **70/70 and healthy** throughout, so every press of it changed nothing, and
/// `HEAL` was never in it: its precondition reads the live party and answers no. What was on the
/// pad was the dialog's `NEXT`, `YES`, `NO` -- two names for one A press -- and `TALK` to get back
/// in, whose ledger entry was read one tile shorter than its own precondition and so was never
/// written.
///
/// The claims, none of them about where the fly goes next:
///
/// - `YES` starts **under five** in the whole run, against 1,278 in the rung-9 hunt from the same
/// room. Not zero: the fly may legitimately answer a hurt party's prompt.
/// - `NEXT` is on **no** pad while a readable YES/NO prompt is open (12.10 in a dialog).
/// - `TALK` is on **no** pad while the fly faces a nurse the party has no use for.
/// - the fly **leaves map 0x3a** on a bounded number of macros.
///
/// ```sh
/// FLY_ROM=/path/to/pokemon-red.gb \
/// FLY_CENTER_CHECKPOINT=.local/checkpoints/release-rank10-pokecenter.checkpoint \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture
/// ```
#[test]
fn the_fly_leaves_the_pokemon_center_from_the_rung_ten_checkpoint() {
let rom = skip_without_rom!();
let Some(checkpoint) = center_checkpoint() else {
eprintln!("skipped: no FLY_CENTER_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
let from = run.map();
assert_eq!(from, PEWTER_POKECENTER, "the checkpoint is the room the stream stalled in");
// The premise of the whole trap: there was nothing to heal.
assert!(run.party_rested(), "the checkpoint's party is already full and healthy");
let mut left = None;
for frame in 0..120_000u32 {
run.frame();
if left.is_none() && run.map() != from {
left = Some(frame);
}
}
eprintln!(
"from map {from:#04x} in {:.1} brain minutes: route {:?}, macros {:?}, dialog frames {} \
(prompt on {}), blocked {:?}",
run.ms / 60_000.0,
run.route,
run.started,
run.dialog_frames,
run.prompt_frames,
run.blocked
);
eprintln!("macros spent in the centre: {:?}", run.started_on_the_first_map);
assert!(
!run.next_on_a_prompt,
"`NEXT` was on the pad at a YES/NO box, where an A press is `YES`"
);
assert!(
!run.talk_at_a_rested_nurse,
"`TALK` was on the pad at a nurse the party had no use for"
);
// In the **centre**, which is what row 41 is about: the run goes on to leave Pewter's gym
// door and fight there, and the gym's own boxes are the fly playing the game rather than the
// ring. 1,278 of 1,295 in the rung-9 hunt from this room; 2,142 live.
let yes = run.started_on_the_first_map.get("YES").copied().unwrap_or(0);
assert!(yes < 5, "`YES` started {yes} times in the centre: {:?}", run.started_on_the_first_map);
let Some(left) = left else {
panic!("the fly never left map {from:#04x}: {:?}", run.started)
};
eprintln!("it left map {from:#04x} on frame {left}");
assert!(
run.macros_on_the_first_map < 400,
"leaving the centre cost {} macros",
run.macros_on_the_first_map
);
}
/// The rung-10 Pewter checkpoint, or `None` to skip.
fn pewter_checkpoint() -> Option<flysim::store::Checkpoint> {
std::env::var_os("FLY_PEWTER_CHECKPOINT").map(|path| {
flysim::store::load(std::path::Path::new(&path))
.expect("the checkpoint should be a FLYSIM01 envelope")
})
}
/// From the rung-10 Pewter checkpoint: the fly gets out of the museum and into the gym.
///
/// **What was live** (2026-09-22, v0.4.5, rank 10 PEWTER CITY, five and a half hours on the
/// rung): the fly on **map 0x34**, the museum's ground floor, and since the 11:30 restart the
/// macro starts were `GO FRONTIER` **1,235**, `BACK` **678**, `GO OBJECTIVE` 267, `GO OUT` 242,
/// `YES` 169 -- with the objective, rung 11's BOULDER BADGE, two doors away in map `0x36`. Two
/// "Stuck" rollbacks fired inside the last half hour and put the fly back where it started.
///
/// **What the survey found** (`infra/docs/macros-traps.md`, 2026-09-22, and
/// `examples/scene_probe.rs`): three things, none of them the text box the brief expected.
///
/// - `BACK` was not in a text box at all. It is on no overworld and no dialog pad, so every one
/// of those 678 was `Scene::Unknown` -- which is *also* what a frame of the overworld reads as
/// while the cartridge drives the fly through a door. An A and a B press into somebody else's
/// script (section 12.13).
/// - the museum had **no row on the map graph**, so from inside it `next_hop` answered nothing
/// and `GO OBJECTIVE` had no goal: the road to the gym did not exist indoors (12.7's rule, and
/// the residual the previous review named).
/// - the museum's frontier is 39 tiles the run has never stood on, of which almost all are
/// behind the admission desk: unreachable, refused `no route`, excluded for ten brain minutes
/// by the blocked ledger and then offered again, once per hold (section 12.14).
///
/// The claims, none of them about which button the fly presses:
///
/// - the fly reaches **map 0x36**, the gym's own interior, on a bounded number of macros;
/// - it stands **facing the thing the rung names** while it is there, which is all `GO OBJECTIVE`
/// ever promises (12.5);
/// - `BACK` on a `dialog` or `unknown` frame stays **under five**, against 189 in the hunt;
/// - no frame deals a pad on an `Unknown` with nothing drawn on it.
///
/// ```sh
/// FLY_ROM=/path/to/pokemon-red.gb \
/// FLY_PEWTER_CHECKPOINT=.local/checkpoints/release-rank10-pewter.checkpoint \
/// cargo test --release -p flysim --test rom_macros_mode -- --nocapture
/// ```
#[test]
fn the_fly_reaches_the_pewter_gym_from_the_rung_ten_checkpoint() {
let rom = skip_without_rom!();
let Some(checkpoint) = pewter_checkpoint() else {
eprintln!("skipped: no FLY_PEWTER_CHECKPOINT");
return;
};
let mut run = Run::resume(&rom, MacroMode::Macros, &checkpoint);
let from = run.map();
assert_eq!(from, MUSEUM_1F, "the checkpoint is the room the stream stalled in");
let mut reached = None;
let mut macros_to_the_gym = 0;
for frame in 0..200_000u32 {
run.frame();
if reached.is_none() {
macros_to_the_gym = run.started.values().sum::<u32>();
if run.map() == PEWTER_GYM {
reached = Some(frame);
}
}
}
eprintln!(
"from map {from:#04x} in {:.1} brain minutes: route {:?}, macros {:?}, GO FRONTIER by \
map {:?}, TALK on the pad on {:?}, TALK starts {:?}, BACK in a box {}, unknown pads \
with no box {}",
run.ms / 60_000.0,
run.route,
run.started,
run.frontier_by_map,
run.talk_on_pad_by_map,
run.talk_starts_by_map,
run.back_in_a_box,
run.unknown_pads_with_no_box
);
let Some(frame) = reached else {
panic!(
"the fly never reached the gym (map {:#04x}, route {:?}, macros {:?})",
run.map(),
run.route,
run.started
)
};
let progress = run.adapter.progress();
eprintln!(
"it reached map {PEWTER_GYM:#04x} on frame {frame}, on {macros_to_the_gym} macros; \
the run ends on rank {} ({}) with {} badges",
progress.rank,
progress.rank_label,
run.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wObtainedBadges).count_ones()
);
assert!(
macros_to_the_gym < 600,
"reaching the gym cost {macros_to_the_gym} macros: {:?}",
run.started
);
assert!(
run.talk_on_pad_by_map.contains(&PEWTER_GYM),
"the fly never stood in front of one of the gym's people: {:?}",
run.talk_on_pad_by_map
);
assert!(
run.objective_on_pad.contains(&PEWTER_GYM),
"`GO OBJECTIVE` had nothing to aim at inside the gym: {:?}",
run.objective_on_pad
);
// Section 12.13, against the 189 the hunt measured on map 0x02 alone.
assert!(
run.back_in_a_box < 5,
"`BACK` was dealt {} times on a dialog or unknown frame",
run.back_in_a_box
);
assert_eq!(
run.unknown_pads_with_no_box, 0,
"a pad was dealt on an unknown frame with nothing drawn on it"
);
// Section 12.14: the museum's frontier is proved unreachable once and then left alone. The
// live run started GO FRONTIER 1,235 times over these two floors and a town.
let museum: u32 = run.frontier_by_map.get(&MUSEUM_1F).copied().unwrap_or(0)
+ run.frontier_by_map.get(&MUSEUM_2F).copied().unwrap_or(0);
assert!(
museum < 40,
"`GO FRONTIER` started {museum} times on the museum's floors: {:?}",
run.frontier_by_map
);
assert!(
run.route.contains(&PEWTER_CITY),
"the road to the gym is out of the museum's front door: {:?}",
run.route
);
}