From 8ef5dc321d30c03fdbd798181bcae659522ab775 Mon Sep 17 00:00:00 2001 From: acamilo Date: Tue, 22 Sep 2026 14:57:34 +0000 Subject: [PATCH] macros: the Pewter run bounds the walks that complete without moving Section 12.2's rule as a number on the cartridge: the longest chain of `GO ...` macros that completed at a net of zero tiles. One is ordinary -- a walk that arrives by turning, a walk cut short by a battle, a walk that finds its goal underfoot -- and a chain of them is the loop the v0.4.5 after arm spent brain minutes 1.0 to 8.5 in. Three is the bound. --- .../crates/flysim/tests/rom_macros_mode.rs | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/services/flysim/crates/flysim/tests/rom_macros_mode.rs b/services/flysim/crates/flysim/tests/rom_macros_mode.rs index e1ab6e6..817aa36 100644 --- a/services/flysim/crates/flysim/tests/rom_macros_mode.rs +++ b/services/flysim/crates/flysim/tests/rom_macros_mode.rs @@ -295,6 +295,24 @@ struct Run { talk_starts_by_map: std::collections::BTreeMap, /// `GO FRONTIER` starts by map, for the museum (section 12.14). frontier_by_map: std::collections::BTreeMap, + /// Where the macro that is running started, for the net-tiles measure below. + started_at: Option<(u32, u8, u8)>, + /// The longest chain of macros that **completed at a net of zero tiles**, and the names in it. + /// + /// Section 12.2's rule, measured (`infra/docs/macros-traps.md` row 54): "a macro that + /// completes without moving because its precondition is already satisfied where the fly stands + /// is a trap". One is ordinary -- a `TALK`, a `NEXT`, a walk that ends where it began because + /// it arrived by turning -- and a *chain* of them is the loop: the after arm of the v0.4.5 + /// hunt spent brain minutes 1.0 to 8.5 cycling `GO FRONTIER`, `GO HEAL` and `GO ROUTE` over + /// five tiles, `GO HEAL` 204 starts at a mean net of 0.0 and a mean reach of 0.0. + /// + /// Counted over walks only, because the presses are supposed to stand still: a `YES` that + /// answers a box and a `MOVE 2` that picks a move both finish on the tile they started on and + /// neither is going anywhere. + net_zero_streak: u32, + worst_net_zero_streak: u32, + net_zero_chain: Vec<&'static str>, + worst_net_zero_chain: Vec<&'static str>, } impl Run { @@ -396,6 +414,11 @@ impl Run { talk_on_pad_by_map: std::collections::BTreeSet::new(), talk_starts_by_map: std::collections::BTreeMap::new(), frontier_by_map: std::collections::BTreeMap::new(), + started_at: None, + net_zero_streak: 0, + worst_net_zero_streak: 0, + net_zero_chain: Vec::new(), + worst_net_zero_chain: Vec::new(), menu_alternation: 0, last_start: None, } @@ -502,6 +525,11 @@ impl Run { talk_on_pad_by_map: std::collections::BTreeSet::new(), talk_starts_by_map: std::collections::BTreeMap::new(), frontier_by_map: std::collections::BTreeMap::new(), + started_at: None, + net_zero_streak: 0, + worst_net_zero_streak: 0, + net_zero_chain: Vec::new(), + worst_net_zero_chain: Vec::new(), menu_alternation: 0, last_start: None, } @@ -731,7 +759,7 @@ impl Run { 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 (mask, started, blocked, done) = { let ledger = AdapterLedger(&self.adapter); let decision = self.layer.decide(&active, 0, self.ms, &mut self.gb, &ledger); let started: Vec<&'static str> = decision @@ -748,8 +776,39 @@ impl Run { }) .map(|event| event.name) .collect(); - (decision.mask, started, blocked) + let done: Vec<&'static str> = decision + .events + .iter() + .filter(|event| { + event.outcome.is_some_and(|outcome| outcome.as_str() == "done") + }) + .map(|event| event.name) + .collect(); + (decision.mask, started, blocked, done) }; + // Row 54's own measure, taken before the starts below so that a macro that finishes and + // another that starts on the same frame are not confused for one another. + for name in done { + let walk = name.starts_with("GO "); + let net = match self.started_at { + Some((map, x, y)) if map == self.map() => { + u32::from(x.abs_diff(self.tile().0)) + u32::from(y.abs_diff(self.tile().1)) + } + // Another map is the biggest move a macro can make. + _ => u32::MAX, + }; + if walk && net == 0 { + self.net_zero_streak += 1; + self.net_zero_chain.push(name); + if self.net_zero_streak > self.worst_net_zero_streak { + self.worst_net_zero_streak = self.net_zero_streak; + self.worst_net_zero_chain = self.net_zero_chain.clone(); + } + } else if walk { + self.net_zero_streak = 0; + self.net_zero_chain.clear(); + } + } for name in blocked { *self.blocked.entry(name).or_insert(0) += 1; let sub = self.battle_sub_state(); @@ -818,6 +877,8 @@ impl Run { let map = self.map(); *self.talk_starts_by_map.entry(map).or_insert(0) += 1; } + let (x, y) = self.tile(); + self.started_at = Some((self.map(), x, y)); } self.gb.set_buttons(mask as u8); self.gb.run_frame().expect("a frame should complete"); @@ -900,6 +961,14 @@ impl Run { } } + /// The tile the fly is standing on, as the macro layer reads it. + fn tile(&mut self) -> (u8, u8) { + ( + self.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wXCoord), + self.gb.read_wram(flybrain_gb::pokemon_red::symbols::ram::wYCoord), + ) + } + /// 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 { @@ -2380,4 +2449,18 @@ fn the_fly_reaches_the_pewter_gym_from_the_rung_ten_checkpoint() { "the road to the gym is out of the museum's front door: {:?}", run.route ); + // Row 54: the cycle the last branch's after arm left behind. `GO FRONTIER`, `GO HEAL` and + // `GO ROUTE` each ended where they began, for seven and a half brain minutes over five tiles. + // Three in a row is the bound: a walk that arrives by turning, a walk cut short by a battle + // and a walk that finds its goal underfoot are each ordinary on their own. + eprintln!( + "the longest chain of walks that completed at a net of zero tiles: {} ({:?})", + run.worst_net_zero_streak, run.worst_net_zero_chain + ); + assert!( + run.worst_net_zero_streak <= 3, + "{} walks in a row completed without moving the fly: {:?}", + run.worst_net_zero_streak, + run.worst_net_zero_chain + ); }