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.
This commit is contained in:
flybrain 2026-09-23 00:37:44 +00:00
parent 5f9237efcd
commit 886509b3b5
2 changed files with 79 additions and 13 deletions

View file

@ -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

View file

@ -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,