From 886509b3b5ffefa9c858026ba0ea34c9a34ff7c5 Mon Sep 17 00:00:00 2001 From: flybrain Date: Wed, 23 Sep 2026 00:37:44 +0000 Subject: [PATCH] a `NO` inside a conversation declines nothing, so it does not un-arm the talk The other half of row 56. The Pewter Gym guide's conversation is fifty-two boxes long and a third of the presses that walk it are `NO`, whose B advances a plain box exactly as `NEXT`'s A does. Every one of them cleared the pending `TALK`, so the talked ledger never learned the conversation had happened, `TALK` stayed on the overworld pad, and an A press at him reopened the whole ring: thirty brain minutes of scene `dialog` with no walk macro dealt. 12.4's rule -- "the fly said no, so the thing is still on offer" -- is about a declined *offer*, and which of the two a `NO` was is decided where it can be seen: by whether the box closes on it. So the decision moves to the frame the text goes away, which is where the talked entry is written anyway, and the reading is `pending_answer`: armed only by an answer to a prompt this crate can read, alive for one hold. A declining `NO` still standing there is a `NO` the box closed on; anything else is a conversation walked through to its end. --- .../src/pokemon_red/macros/executor.rs | 29 +++++++-- .../src/pokemon_red/macros/tests.rs | 63 ++++++++++++++++--- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs index aa00350..4d90f58 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/executor.rs @@ -959,10 +959,26 @@ impl MacroMachine { // the text is gone. if class(state.scene()) != Class::Talking { self.pending_talk = None; - self.talked = Some((pending.map, pending.target)); + // A conversation the fly *declined its way out of* is not a conversation it has had: + // whatever it said no to is still on offer, which is section 12.4's rule and the one + // 12.12 inverts for the nurse alone. The box closing is what tells that apart from a + // `NO` pressed dozens of boxes deep, and [`MacroMachine::pending_answer`] is the + // reading: it is armed only by an answer to a prompt this crate can read and it lives + // for one hold, so a declining `NO` still standing here is a `NO` this box closed on. + if !self.declined_out_of(pending.map) { + self.talked = Some((pending.map, pending.target)); + } } } + /// Whether the answer still standing on `map` is a `NO` to a readable prompt: a declined offer + /// rather than a conversation walked through (section 12.20). + fn declined_out_of(&self, map: u8) -> bool { + self.pending_answer.is_some_and(|pending| { + pending.answered.map == map && pending.answered.prompt && !pending.answered.yes + }) + } + /// One frame after a `YES` or `NO`: decide whether the box it answered has come straight back. /// /// `docs/design/macros.md` section 12.12. The evidence is all in one frame: the fly is on the @@ -1099,11 +1115,12 @@ impl MacroMachine { { self.pending_talk = Some(PendingTalk { map, target, at }); } - // The fly said no. Whatever it said no to is still on offer, so the thing it - // was facing is not retired. - if active.kind == MacroKind::No { - self.pending_talk = None; - } + // The fly said no, and whether that retires what it was facing is decided + // when the box closes rather than here (section 12.20). A `NO` inside a + // conversation is the B that advances a plain box -- it declines nothing -- + // and clearing the pending talk on it kept the Pewter Gym guide `untalked` + // for thirty brain minutes: his conversation is fifty-two boxes long and + // about a third of the presses that walk it are `NO`. // An answer, and the box it answered: armed so that the same prompt coming // straight back is recorded (section 12.12), and the nurse written into the // talked ledger when what was declined was *her* offer. That is 12.4's rule diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs index ea01aaa..72aa2a6 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/macros/tests.rs @@ -3410,9 +3410,12 @@ fn a_conversation_that_walks_the_fly_off_its_tile_is_not_talked_to() { } #[test] -fn a_fly_that_answers_no_has_not_talked_to_anything() { +fn a_fly_that_declines_an_offer_has_not_talked_to_anything() { // The catching tutorial's own shape: a yes/no box, and `NO` is a real answer the pad has to be // able to give. What it must not do is retire the thing that asked -- the offer stands. + // + // Since row 56 the box has to be a **readable prompt** for that to be the reading: a `NO` on a + // plain text box declines nothing, and the test below is the other half. let mut world = World::room().at(3, 3); world.facing = Facing::Down; world.npcs = vec![Npc { slot: 4, picture: 1, x: 3, y: 4, facing: Facing::Up }]; @@ -3422,23 +3425,69 @@ fn a_fly_that_answers_no_has_not_talked_to_anything() { while machine.step(&mut world).is_some() { world.frame(buttons::NONE); } - // The box is open, so the scene is a dialog and the pad is the dialog's. + // The box is open and it is the choice, so the scene is a dialog and the pad is its answers. world.scene = Scene::Dialog; + world.prompt = true; let (dialog, no) = pick(&mut world, MacroKind::No); - // Both answers are on the pad, whatever the box is: there is no WRAM observable for "a choice - // is open" (section 12.2, row 15). - assert!(names(&dialog).contains(&"YES"), "{:?}", names(&dialog)); - assert!(names(&dialog).contains(&"NO"), "{:?}", names(&dialog)); + assert_eq!(names(&dialog), ["YES", "NO"], "a readable choice deals its own answers (12.12)"); machine.start(&dialog, no, &mut world).expect("NO is bound in a dialog"); while machine.step(&mut world).is_some() { world.frame(buttons::NONE); } world.scene = Scene::Overworld; machine.observe_frame(&mut world); - assert_eq!(machine.take_talked(), None, "a no is not a conversation had"); + assert_eq!(machine.take_talked(), None, "a declined offer is not a conversation had"); assert!(on_the_pad(&mut world, MacroKind::Talk), "the offer stands"); } +#[test] +fn a_no_pressed_inside_a_conversation_is_not_a_declined_offer() { + // Row 56, the Pewter Gym guide. His conversation is fifty-two boxes long and `NO`'s B advances + // a plain one exactly as `NEXT`'s A does -- it declines nothing. Clearing the pending `TALK` on + // it meant the talked ledger never learned the conversation had happened, so `TALK` was on the + // overworld pad every hold, and an A press at him reopened the whole ring: thirty brain + // minutes of scene `dialog` with no walk macro dealt at all. + // + // Which of the two a `NO` was is decided where it can be seen: by whether the box closes on it. + let mut world = World::room().at(3, 3); + world.facing = Facing::Down; + world.npcs = vec![Npc { slot: 4, picture: 1, x: 3, y: 4, facing: Facing::Up }]; + let mut machine = MacroMachine::new(1); + let (palette, slot) = pick(&mut world, MacroKind::Talk); + machine.start(&palette, slot, &mut world).expect("TALK is bound at a person"); + while machine.step(&mut world).is_some() { + world.frame(buttons::NONE); + } + + // Deep inside the conversation: a plain text box, not a choice. + world.scene = Scene::Dialog; + world.prompt = false; + let (dialog, no) = pick(&mut world, MacroKind::No); + assert!(names(&dialog).contains(&"NEXT"), "a plain box deals all three: {:?}", names(&dialog)); + machine.start(&dialog, no, &mut world).expect("NO is bound in a dialog"); + while machine.step(&mut world).is_some() { + world.frame(buttons::NONE); + } + + // The box is still open, so nothing is decided yet -- the conversation is still running. + machine.observe_frame(&mut world); + assert_eq!(machine.take_talked(), None, "the box is still open"); + + // And when the text is gone the conversation counts, which is what shuts the ring's door. + world.scene = Scene::Overworld; + machine.observe_frame(&mut world); + assert_eq!( + machine.take_talked(), + Some((world.map, TalkTarget::Sprite(4))), + "a conversation walked through to its end is a conversation had" + ); + world.talked.insert(TalkTarget::Sprite(4)); + assert!( + !on_the_pad(&mut world, MacroKind::Talk), + "`TALK` is the ring's door and this run has been through it" + ); +} + #[test] fn a_walk_the_cartridge_pushes_back_excludes_what_it_was_walking_to() { // Row 28's other half. Every macro that walked into the gate ended `Done` -- a scene change,