From a6a8f797ffabf44c7b60ffa0c3f0911fa6eb82c9 Mon Sep 17 00:00:00 2001 From: flybrain Date: Wed, 23 Sep 2026 00:30:35 +0000 Subject: [PATCH] survey: the conversation probe, and every rectangle the cartridge drew Row 56 is a gym's dialog for thirty brain minutes with YES, NO and NEXT dealt in equal thirds, so the question is which box each press is answering and whether the seam can see it at all. `state::drawn_boxes` asks the screen instead of a pinned rectangle: every complete `TextBoxBorder` on the frame. `FLY_PROBE_CATCH=dialog` walks the conversation one raw pulse at a time and prints both readings of every frame side by side, with the pad the palette deals for it, and tallies how the candidate readings separate the frames. `FLY_PROBE_ANSWER` picks the button a frame with a box on it is answered with, so the two arms of a choice can be walked separately. --- .../flybrain-gb/src/pokemon_red/state.rs | 28 +++ .../crates/flysim/examples/scene_probe.rs | 159 ++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs index 82c5be5..9ed92a1 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/state.rs @@ -680,6 +680,34 @@ pub fn dialog_border(memory: &mut dyn MemoryReader) -> (bool, bool) { (box_drawn(memory, 0, 12, 19, 17), border_drawn(memory, 0, 12, 19, 17)) } +/// Every complete [`border_drawn`] rectangle on screen, as `(left, top, right, bottom)`. +/// +/// A diagnostic, beside [`dialog_border`], and the reading row 56 turns on. The dialogue box and +/// the two-option box are both read at *pinned* coordinates, because that is where the scripts +/// that draw them put them — so a prompt Red drew somewhere else is invisible to +/// [`yes_no_prompt`], and "invisible" and "not there" are the same answer from inside the seam. +/// This asks the screen instead: which rectangles on this frame are whole `TextBoxBorder` +/// figures. Every rectangle at least three by three is tried, which is 130,000 reads of a +/// memoized buffer and is a survey tool rather than a per-frame accessor. +pub fn drawn_boxes(memory: &mut dyn MemoryReader) -> Vec<(u16, u16, u16, u16)> { + let mut found = Vec::new(); + for top in 0..poke::SCREEN_HEIGHT { + for left in 0..poke::SCREEN_WIDTH { + if screen_tile(memory, left, top) != poke::frame::TOP_LEFT { + continue; + } + for bottom in (top + 2)..poke::SCREEN_HEIGHT { + for right in (left + 2)..poke::SCREEN_WIDTH { + if border_drawn(memory, left, top, right, bottom) { + found.push((left, top, right, bottom)); + } + } + } + } + } + found +} + pub fn dialog_corners(memory: &mut dyn MemoryReader) -> [u8; 4] { [ screen_tile(memory, 0, 12), diff --git a/services/flysim/crates/flysim/examples/scene_probe.rs b/services/flysim/crates/flysim/examples/scene_probe.rs index db873b1..9648ced 100644 --- a/services/flysim/crates/flysim/examples/scene_probe.rs +++ b/services/flysim/crates/flysim/examples/scene_probe.rs @@ -1202,6 +1202,158 @@ fn shop_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) } } +/// Everything that tells one box of a conversation from another, on one line (row 56). +/// +/// The two readings side by side: what the seam makes of the frame ([`state::yes_no_prompt`], and +/// the scene the pad is dealt for), and **where the cartridge actually drew a box** +/// ([`state::drawn_boxes`]). A prompt Red draws somewhere other than the nurse's corner reads +/// `yesno=false` on the left of that line and shows up as a rectangle on the right of it, which is +/// the whole question row 56 asks. +fn dialog_frame(gb: &mut Emulator) -> String { + let text = state::text_box(gb); + let boxes: Vec = state::drawn_boxes(gb) + .into_iter() + .map(|(left, top, right, bottom)| format!("({left},{top})-({right},{bottom})")) + .collect(); + format!( + "{:?} open={} waiting={} yesno={} cursor=({},{},{},{},{:#04x}) textbox={:#04x} \ + boxes=[{}] | {} | {}", + scene::detect(gb), + text.open, + text.waiting, + state::yes_no_prompt(gb), + gb.read8(ram::wTopMenuItemY), + gb.read8(ram::wTopMenuItemX), + gb.read8(ram::wCurrentMenuItem), + gb.read8(ram::wMaxMenuItem), + gb.read8(ram::wMenuWatchedKeys), + gb.read8(ram::wTextBoxID), + boxes.join(" "), + box_line(gb, 14), + box_line(gb, 16), + ) +} + +/// The pad the palette deals for the frame that is up, by name. +fn dialog_pad(gb: &mut Emulator, adapter: &PokemonRedReward) -> String { + use flybrain_gb::pokemon_red::macros::cartridge::MacroState; + use flybrain_gb::pokemon_red::macros::plan; + let ledger = AdapterLedger(adapter); + let mut poke = flybrain_gb::pokemon_red::state::PokeState::with_ledger(gb, &ledger); + let state: &mut dyn MacroState = &mut poke; + let scene = state.scene(); + let plan = plan::plan_for(scene, state); + let names: Vec<&str> = plan.slots.iter().flatten().map(|spec| spec.name).collect(); + format!("{names:?}") +} + +/// Row 56's conversation survey: which box the fly is answering in a gym, and where Red draws it. +/// +/// `FLY_PROBE_CATCH=dialog`. The live loop was map 54, scene `dialog`, `YES` 64 / `NO` 62 / +/// `NEXT` 59 / `TALK` 6 over ten brain minutes with no walk macro dealt at all, and three readings +/// fit that: a conversation that re-offers its choice for ever, a talked ledger that never records +/// the person, or a prompt the seam cannot see. They are told apart by walking the conversation +/// press by press and printing both readings of every frame, so this walks it. +/// +/// `FLY_PROBE_ANSWER` picks the button the survey presses on a frame where a box **is** drawn +/// somewhere: `a` (the default) or `b`. Everything else gets an A, because A is what advances a +/// plain box. The pad is printed beside each frame, so a row where the pad is `NEXT, YES, NO` on a +/// frame with a two-option box on screen is the trap said out loud. +fn dialog_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) { + use flybrain_gb::pokemon_red::macros::cartridge::MacroState; + use flybrain_gb::pokemon_red::macros::palette; + + let pulse = |gb: &mut Emulator, adapter: &mut PokemonRedReward, mask: u8, ms: &mut f64| { + for phase in 0..16 { + gb.set_buttons(if phase < 8 { mask } else { 0 }); + gb.run_frame().expect("a frame should complete"); + *ms += MS_PER_FRAME; + adapter.sample(gb, *ms); + } + }; + + println!("\n## What the fly is standing on, and what it is facing\n"); + { + let ledger = AdapterLedger(adapter); + let mut poke = flybrain_gb::pokemon_red::state::PokeState::with_ledger(gb, &ledger); + let state: &mut dyn MacroState = &mut poke; + println!("- player: {:?}", state.player()); + println!("- objective: {:?}", state.objective()); + println!("- facing: {:?}", palette::facing_target(state)); + println!("- `facing_untalked` = {}", palette::facing_untalked(state)); + println!("- untalked people: {:?}", palette::untalked_people(state)); + println!("- untalked objects: {:?}", palette::untalked_objects(state)); + println!("- `yes_no_prompt` = {}", state.yes_no_prompt()); + } + println!("\n## The screen at the checkpoint\n\n```\n{}\n```\n", screen_rows(gb)); + println!("```"); + for row in screen_text(gb) { + println!("{row}"); + } + println!("```\n"); + println!("- the frame: {}", dialog_frame(gb)); + println!("- the pad: {}", dialog_pad(gb, adapter)); + + let answer = std::env::var("FLY_PROBE_ANSWER").unwrap_or_else(|_| "a".to_string()); + let answer_mask = + if answer == "b" { flybrain_gb::buttons::B } else { flybrain_gb::buttons::A }; + + println!( + "\n## The conversation, one raw pulse at a time (a box on screen gets `{answer}`)\n" + ); + println!("```"); + let mut last = String::new(); + let mut boxes_seen: BTreeMap = BTreeMap::new(); + let mut classes: BTreeMap<(bool, bool, bool), u64> = BTreeMap::new(); + for index in 0..env_usize("FLY_PROBE_PULSES", 200) { + let choice = if state::drawn_boxes(gb).iter().any(|(_, top, _, _)| *top < 12) { + answer_mask + } else { + flybrain_gb::buttons::A + }; + pulse(gb, adapter, choice, ms); + let drawn = state::drawn_boxes(gb); + for (left, top, right, bottom) in &drawn { + *boxes_seen.entry(format!("({left},{top})-({right},{bottom})")).or_default() += 1; + } + *classes + .entry(( + drawn.iter().any(|(_, top, _, _)| *top < 12), + gb.read8(ram::wTextBoxID) == 0x14, + state::yes_no_prompt(gb), + )) + .or_default() += 1; + let now = format!("{} pad={}", dialog_frame(gb), dialog_pad(gb, adapter)); + if now != last { + println!("#{index:<3} {now}"); + last = now; + } + } + println!("```\n"); + println!("Every rectangle the cartridge drew over the survey, and on how many frames:\n"); + for (figure, count) in &boxes_seen { + println!("- `{figure}` on {count} frames"); + } + println!("\n{}", separator_table(&classes)); +} + +/// How the candidate readings of "a two-option box is up" separate the frames of a survey. +/// +/// Three columns, because three things could say it and only a measurement says which: the +/// cartridge's own `wTextBoxID`, the seam's pinned-geometry [`state::yes_no_prompt`], and the +/// generalised reading -- a complete border drawn around the cursor the game parked, wherever on +/// screen that is. A row where the box is drawn and a column reads `false` is that column missing +/// the prompt. +fn separator_table(classes: &BTreeMap<(bool, bool, bool), u64>) -> String { + let mut out = String::from( + "| a box drawn above the dialogue box | `wTextBoxID` = `$14` | `yes_no_prompt` | frames |\n | --- | --- | --- | ---: |\n", + ); + for ((drawn, textbox, prompt), frames) in classes { + out.push_str(&format!("| {drawn} | {textbox} | {prompt} | {frames} |\n")); + } + out +} + fn main() { let Some(path) = std::env::var_os("FLY_ROM") else { println!("FLY_ROM is not set, so there is nothing to probe."); @@ -1277,6 +1429,13 @@ fn main() { return; } + // Row 56's conversation survey: which box a gym's guide draws, where he draws it, and what + // the dialog pad makes of it press by press. + if std::env::var("FLY_PROBE_CATCH").is_ok_and(|value| value == "dialog") { + dialog_survey(&mut gb, &mut adapter, &mut ms); + return; + } + let budget = env_usize("FLY_PROBE_FRAMES", 200_000); let stuck_after = env_usize("FLY_PROBE_STUCK", 600); let mut next_burst = ms;