macros: tests for the nurse's box
Unit: HEAL is off a full party's pad, including the rung-10 party's own numbers, which is the assertion that the loop was never the heal's; TALK is off a rested nurse's pad and on a hurt one's, and an ordinary villager is unaffected; her prompt deals one answer and a plain box still deals three; a readable prompt that is not hers deals both answers and no NEXT; an answer whose prompt reopens is excluded and one that settled the box is not; a completed heal and a declined prompt both retire her. ROM-gated from the live checkpoint (FLY_CENTER_CHECKPOINT): the fly leaves map 0x3a on a bounded number of macros, YES starts stay under five, NEXT is on no pad while a prompt is readable, and TALK is on no pad at a rested nurse. The run also counts dialog and prompt frames, which is what makes the forty-six-to-one ratio visible.
This commit is contained in:
parent
70a2433e25
commit
0bf4b965fb
2 changed files with 334 additions and 1 deletions
|
|
@ -27,7 +27,8 @@ use super::executor::{
|
||||||
WALK_FRAME_CEILING, walk_budget,
|
WALK_FRAME_CEILING, walk_budget,
|
||||||
};
|
};
|
||||||
use super::palette::{
|
use super::palette::{
|
||||||
MacroId, MacroKind, Palette, SLOTS, amenity_goals, errand, healthiest_other, heal_goals,
|
MacroId, MacroKind, Palette, SLOTS, amenity_goals, answer_key, errand, facing_nurse,
|
||||||
|
healthiest_other, heal_goals, nurse_prompt, rested_nurse,
|
||||||
listing, losing, move_slot_bound, objective_goals, party_needs_rest, party_rested,
|
listing, losing, move_slot_bound, objective_goals, party_needs_rest, party_rested,
|
||||||
poke_sprite, precondition, throw_slot, untalked_objects, untalked_people, ways,
|
poke_sprite, precondition, throw_slot, untalked_objects, untalked_people, ways,
|
||||||
};
|
};
|
||||||
|
|
@ -163,6 +164,12 @@ struct World {
|
||||||
/// A frame at which the cartridge heals the party, which is what a Pokémon Center does while
|
/// A frame at which the cartridge heals the party, which is what a Pokémon Center does while
|
||||||
/// its text box is open (`docs/design/macros.md` section 13).
|
/// its text box is open (`docs/design/macros.md` section 13).
|
||||||
heal_at: Option<u32>,
|
heal_at: Option<u32>,
|
||||||
|
/// Whether the two-option YES/NO box is the thing on screen ([`MacroState::yes_no_prompt`]).
|
||||||
|
///
|
||||||
|
/// A field rather than a shape of the `list`, because on the cartridge it is a *drawn box*
|
||||||
|
/// beside a cursor the game never clears, and what the palette asks is only "is a choice
|
||||||
|
/// open" (section 12.12).
|
||||||
|
prompt: bool,
|
||||||
/// Whether the cartridge is driving the player right now ([`MacroState::scripted`]).
|
/// Whether the cartridge is driving the player right now ([`MacroState::scripted`]).
|
||||||
scripted: bool,
|
scripted: bool,
|
||||||
/// A frame at which the cartridge takes the joypad, which is what the Viridian gate does.
|
/// A frame at which the cartridge takes the joypad, which is what the Viridian gate does.
|
||||||
|
|
@ -236,6 +243,7 @@ impl World {
|
||||||
pending: None,
|
pending: None,
|
||||||
switch: None,
|
switch: None,
|
||||||
heal_at: None,
|
heal_at: None,
|
||||||
|
prompt: false,
|
||||||
scripted: false,
|
scripted: false,
|
||||||
scripted_at: None,
|
scripted_at: None,
|
||||||
pulses: Vec::new(),
|
pulses: Vec::new(),
|
||||||
|
|
@ -339,6 +347,18 @@ impl World {
|
||||||
world
|
world
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`World::center`] with the fly at the counter facing the nurse, mid-conversation.
|
||||||
|
///
|
||||||
|
/// The rung-10 state (`infra/docs/macros-traps.md` row 41): map `0x3a` at (3, 3) facing up,
|
||||||
|
/// a text box open, the nurse two tiles away over the counter at (3, 1).
|
||||||
|
fn at_the_nurse() -> Self {
|
||||||
|
let mut world = Self::center();
|
||||||
|
world.player = Tile::new(3, 3);
|
||||||
|
world.facing = Facing::Up;
|
||||||
|
world.scene = Scene::Dialog;
|
||||||
|
world
|
||||||
|
}
|
||||||
|
|
||||||
fn at(mut self, x: u8, y: u8) -> Self {
|
fn at(mut self, x: u8, y: u8) -> Self {
|
||||||
self.player = Tile::new(x, y);
|
self.player = Tile::new(x, y);
|
||||||
self
|
self
|
||||||
|
|
@ -638,6 +658,12 @@ impl MacroState for World {
|
||||||
self.scripted
|
self.scripted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A drawn box is what the reading rests on, so a prompt cannot be open with no box open:
|
||||||
|
/// `pokemon_red::state::yes_no_prompt` gates on `wFontLoaded` before it looks at the tiles.
|
||||||
|
fn yes_no_prompt(&mut self) -> bool {
|
||||||
|
self.prompt && self.scene == Scene::Dialog
|
||||||
|
}
|
||||||
|
|
||||||
fn shop_stock(&mut self) -> Vec<u8> {
|
fn shop_stock(&mut self) -> Vec<u8> {
|
||||||
self.stock.clone()
|
self.stock.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -3892,6 +3918,16 @@ fn heal_is_on_the_centres_pad_only_while_the_party_needs_it() {
|
||||||
assert!(!precondition(MacroKind::Heal, &mut center));
|
assert!(!precondition(MacroKind::Heal, &mut center));
|
||||||
assert!(!on_the_pad(&mut center, MacroKind::Heal));
|
assert!(!on_the_pad(&mut center, MacroKind::Heal));
|
||||||
|
|
||||||
|
// The rung-10 party, as the live checkpoint of 2026-09-22 reads it: one Pokemon, 70 of 70,
|
||||||
|
// healthy (`infra/docs/macros-traps.md` row 41). `HEAL` was **not** what looped there -- its
|
||||||
|
// precondition reads the live party and answers no, and the survey confirmed it on the
|
||||||
|
// cartridge. So this is the assertion that the loop was never the heal's.
|
||||||
|
let mut rung10 = World::center();
|
||||||
|
rung10.mons = vec![Mon { hp: 70, max_hp: 70, ..mon(0, 70, 70, &[(33, 30)]) }];
|
||||||
|
assert!(!party_needs_rest(&mut rung10));
|
||||||
|
assert!(!precondition(MacroKind::Heal, &mut rung10));
|
||||||
|
assert!(!on_the_pad(&mut rung10, MacroKind::Heal));
|
||||||
|
|
||||||
// Hurt.
|
// Hurt.
|
||||||
center.mons[0].hp = 9;
|
center.mons[0].hp = 9;
|
||||||
assert!(precondition(MacroKind::Heal, &mut center));
|
assert!(precondition(MacroKind::Heal, &mut center));
|
||||||
|
|
@ -4565,3 +4601,149 @@ fn a_scripted_push_back_records_the_tile_it_happened_on() {
|
||||||
|
|
||||||
mod map_aware;
|
mod map_aware;
|
||||||
mod shop_purchase;
|
mod shop_purchase;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Section 12.12: the nurse's box (row 41)
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn talk_is_off_the_pad_at_a_nurse_the_party_has_no_use_for() {
|
||||||
|
// The overworld frame the ring starts from: at the counter, facing the nurse, party full.
|
||||||
|
let mut center = World::center().at(3, 3);
|
||||||
|
center.facing = Facing::Up;
|
||||||
|
assert!(facing_nurse(&mut center), "the nurse is the thing ahead, over the counter");
|
||||||
|
assert!(rested_nurse(&mut center));
|
||||||
|
assert!(!precondition(MacroKind::Talk, &mut center));
|
||||||
|
assert!(!on_the_pad(&mut center, MacroKind::Talk));
|
||||||
|
|
||||||
|
// Hurt, and she is worth talking to again -- the conversation now does something.
|
||||||
|
center.mons[0].hp = 4;
|
||||||
|
assert!(!rested_nurse(&mut center));
|
||||||
|
assert!(precondition(MacroKind::Talk, &mut center));
|
||||||
|
assert!(on_the_pad(&mut center, MacroKind::Talk));
|
||||||
|
|
||||||
|
// Statused at full HP counts as needing her, exactly as `HEAL`'s own precondition does.
|
||||||
|
center.mons[0].hp = center.mons[0].max_hp;
|
||||||
|
center.mons[0].status = Status::Poison;
|
||||||
|
assert!(precondition(MacroKind::Talk, &mut center));
|
||||||
|
|
||||||
|
// And nobody else in the game is narrowed by this: an ordinary person on the same map is
|
||||||
|
// still `TALK`'s whatever the party reads.
|
||||||
|
let mut villager = World::center().at(3, 3);
|
||||||
|
villager.facing = Facing::Up;
|
||||||
|
villager.npcs = vec![Npc { slot: 1, picture: 1, x: 3, y: 2, facing: Facing::Down }];
|
||||||
|
villager.counters.clear();
|
||||||
|
villager.walls.clear();
|
||||||
|
assert!(!facing_nurse(&mut villager));
|
||||||
|
assert!(precondition(MacroKind::Talk, &mut villager), "a full party is not a reason to ignore a person");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_nurses_prompt_offers_only_the_answer_that_changes_something() {
|
||||||
|
let mut center = World::at_the_nurse();
|
||||||
|
center.prompt = true;
|
||||||
|
assert!(nurse_prompt(&mut center));
|
||||||
|
|
||||||
|
// Full and healthy: the offer is for nothing, so `NO` is the answer and `YES` is not on the
|
||||||
|
// pad. `NEXT` is off it too -- an A press at a two-option box *is* `YES` (12.10).
|
||||||
|
assert_eq!(names(&plan::plan_for(Scene::Dialog, &mut center)), ["NO"]);
|
||||||
|
|
||||||
|
// Hurt: `YES` is the answer, and `NO` is the one that changes nothing.
|
||||||
|
center.mons[0].hp = 4;
|
||||||
|
assert_eq!(names(&plan::plan_for(Scene::Dialog, &mut center)), ["YES"]);
|
||||||
|
|
||||||
|
// A plain text box, which is forty-five of the nurse's forty-six frames, keeps all three: A
|
||||||
|
// and B both advance one and there is no choice for `NEXT` to be the wrong name for.
|
||||||
|
center.prompt = false;
|
||||||
|
assert_eq!(names(&plan::plan_for(Scene::Dialog, &mut center)), ["NEXT", "YES", "NO"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_readable_prompt_that_is_not_the_nurses_keeps_both_answers_and_loses_next() {
|
||||||
|
// Red draws a two-option box for a dozen scripts and only the nurse's is an offer about the
|
||||||
|
// party, so nothing else is narrowed by the party: both answers, and no `NEXT`.
|
||||||
|
let mut world = World::room();
|
||||||
|
world.scene = Scene::Dialog;
|
||||||
|
world.prompt = true;
|
||||||
|
assert!(!nurse_prompt(&mut world));
|
||||||
|
assert_eq!(names(&plan::plan_for(Scene::Dialog, &mut world)), ["YES", "NO"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_yes_no_box_that_reopens_unchanged_takes_that_answer_off_the_pad() {
|
||||||
|
// Section 12.12's general rule, away from the nurse: the answer completed, the fly is on the
|
||||||
|
// tile it answered from, and the same prompt is up again -- so the press did nothing, which
|
||||||
|
// is section 12.2's trap, and the answer joins the blocked ledger for its window.
|
||||||
|
let mut world = World::room();
|
||||||
|
world.scene = Scene::Dialog;
|
||||||
|
world.prompt = true;
|
||||||
|
assert_eq!(names(&plan::plan_for(Scene::Dialog, &mut world)), ["YES", "NO"]);
|
||||||
|
|
||||||
|
assert_eq!(run(&mut world, MacroKind::Yes).unwrap(), MacroAbort::Done);
|
||||||
|
let key = answer_key(&mut world, true).expect("a loaded map has a tile");
|
||||||
|
assert!(world.targets.blocked(world.map, key), "the answer that changed nothing");
|
||||||
|
assert_eq!(
|
||||||
|
names(&plan::plan_for(Scene::Dialog, &mut world)),
|
||||||
|
["NO"],
|
||||||
|
"the other answer is still there, which is what ends the ring"
|
||||||
|
);
|
||||||
|
|
||||||
|
// And `NO` is not excluded by `YES`'s entry: one answer, one key.
|
||||||
|
let no = answer_key(&mut world, false).expect("a loaded map has a tile");
|
||||||
|
assert!(!world.targets.blocked(world.map, no));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_prompt_that_does_not_come_back_excludes_nothing() {
|
||||||
|
// The other half of the same rule: an answer that settled the box is an answer worth making
|
||||||
|
// again. Nothing is excluded, because nothing looped.
|
||||||
|
let mut world = World::room();
|
||||||
|
world.scene = Scene::Dialog;
|
||||||
|
world.prompt = true;
|
||||||
|
// The box closes on the frame after the press, which is what answering it does.
|
||||||
|
world.switch = Some((2, Scene::Overworld));
|
||||||
|
assert_eq!(run(&mut world, MacroKind::Yes).unwrap(), MacroAbort::Done);
|
||||||
|
let key = answer_key(&mut world, true).expect("a loaded map has a tile");
|
||||||
|
assert!(!world.targets.blocked(world.map, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_declined_heal_writes_the_nurse_into_the_talked_ledger() {
|
||||||
|
// 12.4's rule is "the fly said no, so the thing is still on offer", and for one person in Red
|
||||||
|
// that is wrong: the pad only ever offers `NO` at her prompt when the party is already full,
|
||||||
|
// so declining is the errand's end rather than a conversation postponed.
|
||||||
|
let mut center = World::at_the_nurse();
|
||||||
|
center.prompt = true;
|
||||||
|
assert!(party_rested(&mut center));
|
||||||
|
|
||||||
|
assert_eq!(run(&mut center, MacroKind::No).unwrap(), MacroAbort::Done);
|
||||||
|
assert!(center.talked.contains(&TalkTarget::Sprite(1)), "the nurse: {:?}", center.talked);
|
||||||
|
|
||||||
|
// So `TALK` is off the pad there even if the party is hurt later: the ledger is the record
|
||||||
|
// that this run has had her conversation.
|
||||||
|
center.scene = Scene::Overworld;
|
||||||
|
center.mons[0].hp = 4;
|
||||||
|
assert!(!precondition(MacroKind::Talk, &mut center));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_completed_heal_writes_the_nurse_into_the_talked_ledger() {
|
||||||
|
let mut center = World::center();
|
||||||
|
center.mons[0].hp = 3;
|
||||||
|
center.switch = Some((200, Scene::Dialog));
|
||||||
|
center.heal_at = Some(240);
|
||||||
|
|
||||||
|
assert_eq!(run(&mut center, MacroKind::Heal).unwrap(), MacroAbort::Done);
|
||||||
|
assert!(party_rested(&mut center));
|
||||||
|
assert!(
|
||||||
|
center.talked.contains(&TalkTarget::Sprite(1)),
|
||||||
|
"a completed heal has had the conversation: {:?}",
|
||||||
|
center.talked
|
||||||
|
);
|
||||||
|
|
||||||
|
// And `TALK` cannot reopen it. The reached window would expire in ten brain minutes and offer
|
||||||
|
// the fly the same forty-six text frames again; the talked entry is for the session.
|
||||||
|
center.scene = Scene::Overworld;
|
||||||
|
assert_eq!(center.player, Tile::new(3, 3));
|
||||||
|
assert!(!precondition(MacroKind::Talk, &mut center));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ const VIRIDIAN_MART: u32 = 0x2a;
|
||||||
/// Route 2's southern forest gate and the forest north of it, which is rung 9's own road.
|
/// 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_SOUTH_GATE: u32 = 0x32;
|
||||||
const VIRIDIAN_FOREST: u32 = 0x33;
|
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.
|
/// The upper floor of the Pewter museum, which is the building the rung-10 stall was inside.
|
||||||
const MUSEUM_2F: u32 = 0x35;
|
const MUSEUM_2F: u32 = 0x35;
|
||||||
/// The forest's *northern* gate, which is the first hop from the forest toward Pewter
|
/// The forest's *northern* gate, which is the first hop from the forest toward Pewter
|
||||||
|
|
@ -241,6 +243,23 @@ struct Run {
|
||||||
longest_menu_back_alternation: u32,
|
longest_menu_back_alternation: u32,
|
||||||
menu_alternation: u32,
|
menu_alternation: u32,
|
||||||
last_start: Option<&'static str>,
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Run {
|
impl Run {
|
||||||
|
|
@ -332,6 +351,10 @@ impl Run {
|
||||||
blocked_where: std::collections::BTreeSet::new(),
|
blocked_where: std::collections::BTreeSet::new(),
|
||||||
macros_on_the_first_map: 0,
|
macros_on_the_first_map: 0,
|
||||||
longest_menu_back_alternation: 0,
|
longest_menu_back_alternation: 0,
|
||||||
|
dialog_frames: 0,
|
||||||
|
prompt_frames: 0,
|
||||||
|
next_on_a_prompt: false,
|
||||||
|
talk_at_a_rested_nurse: false,
|
||||||
menu_alternation: 0,
|
menu_alternation: 0,
|
||||||
last_start: None,
|
last_start: None,
|
||||||
}
|
}
|
||||||
|
|
@ -428,6 +451,10 @@ impl Run {
|
||||||
blocked_where: std::collections::BTreeSet::new(),
|
blocked_where: std::collections::BTreeSet::new(),
|
||||||
macros_on_the_first_map: 0,
|
macros_on_the_first_map: 0,
|
||||||
longest_menu_back_alternation: 0,
|
longest_menu_back_alternation: 0,
|
||||||
|
dialog_frames: 0,
|
||||||
|
prompt_frames: 0,
|
||||||
|
next_on_a_prompt: false,
|
||||||
|
talk_at_a_rested_nurse: false,
|
||||||
menu_alternation: 0,
|
menu_alternation: 0,
|
||||||
last_start: None,
|
last_start: None,
|
||||||
}
|
}
|
||||||
|
|
@ -574,6 +601,20 @@ impl Run {
|
||||||
flybrain_gb::pokemon_red::macros::MacroState::shop_stock(&mut state)
|
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`.
|
/// Whether every party member reads full HP with no status: the end of a `HEAL`.
|
||||||
fn party_rested(&mut self) -> bool {
|
fn party_rested(&mut self) -> bool {
|
||||||
let party = flybrain_gb::pokemon_red::state::party(&mut self.gb);
|
let party = flybrain_gb::pokemon_red::state::party(&mut self.gb);
|
||||||
|
|
@ -748,6 +789,24 @@ impl Run {
|
||||||
if self.route.last() != Some(&map) && map != u32::MAX {
|
if self.route.last() != Some(&map) && map != u32::MAX {
|
||||||
self.route.push(map);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
// 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
|
// dealing one button. Only the overworld: a warp in flight reads `unknown` and a text box
|
||||||
// is a pad of its own.
|
// is a pad of its own.
|
||||||
|
|
@ -2017,3 +2076,95 @@ fn the_fly_leaves_the_pewter_building_from_the_rung_ten_checkpoint() {
|
||||||
run.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
|
||||||
|
);
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
let yes = run.started.get("YES").copied().unwrap_or(0);
|
||||||
|
assert!(yes < 5, "`YES` started {yes} times: {:?}", run.started);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue