From 2d04abaf524c8d433d6141a20e15394fb7e9acd1 Mon Sep 17 00:00:00 2001 From: flybrain Date: Wed, 23 Sep 2026 06:23:13 +0000 Subject: [PATCH] survey: the hunt can start inside a live session's ledgers, and counts refusals The seeding moves from scene_probe into examples/support/ledgers.rs and trap_hunt reads it as FLY_TRAP_SEED_*, so both arms of a hunt can start inside the state the live session was in: row 57's pad was dealt by ledgers a restore starts empty, and a hunt from the bare checkpoint never meets it. The report gains refusals by macro and the longest run of one macro refused with the fly on one tile. scene_probe's own choice seed is FLY_PROBE_RNG now, so it does not read as one of the FLY_PROBE_SEED_* ledgers. --- .../crates/flysim/examples/scene_probe.rs | 99 ++----------------- .../crates/flysim/examples/support/ledgers.rs | 95 ++++++++++++++++++ .../crates/flysim/examples/trap_hunt.rs | 55 +++++++++++ 3 files changed, 158 insertions(+), 91 deletions(-) create mode 100644 services/flysim/crates/flysim/examples/support/ledgers.rs diff --git a/services/flysim/crates/flysim/examples/scene_probe.rs b/services/flysim/crates/flysim/examples/scene_probe.rs index d7ffb38..5f83667 100644 --- a/services/flysim/crates/flysim/examples/scene_probe.rs +++ b/services/flysim/crates/flysim/examples/scene_probe.rs @@ -46,6 +46,9 @@ use flysim::config::Config; use flysim::macros::macro_layer; use flysim::snapshot::MacroMode; +#[path = "support/ledgers.rs"] +mod ledgers; + const MS_PER_FRAME: f64 = 1000.0 / 59.7275; const BURST_MS: f64 = 100.0; const HOLDS_PER_SLOT: usize = 3; @@ -1337,94 +1340,6 @@ fn dialog_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64 println!("\n{}", separator_table(&classes)); } -/// Rebuild a live session's ledgers from the environment, because a restore starts them empty. -/// -/// `FLY_PROBE_SEED_PUSHED="x,y;x,y"` walls tiles of the loaded map the way a scripted push-back -/// does; `FLY_PROBE_SEED_EXHAUSTED=1` marks its frontier unreachable (section 12.14); -/// `FLY_PROBE_SEED_TALKED=1` writes every person and sign on it into the talked ledger; -/// `FLY_PROBE_SEED_BLOCKED="warp2;east;south"` rests those exits for the window. The tile underfoot is -/// always recorded as stood on. -fn seed_ledgers( - macros: &mut flybrain_gb::pokemon_red::macros::PokemonPalette, - gb: &mut Emulator, - adapter: &PokemonRedReward, - ms: f64, -) { - use flybrain_gb::MacroPalette; - use flybrain_gb::pokemon_red::macros::cartridge::{Edge, ExitId, MacroState, TargetKey}; - use flybrain_gb::pokemon_red::macros::{Tile, path}; - - let Some(player) = state::player(gb) else { return }; - let map = player.map; - macros.clock(ms); - let (things, covered): (Vec<_>, Vec) = { - let ledger = AdapterLedger(adapter); - macros.inspect(gb, &ledger, |state: &mut dyn MacroState| { - let mut all = path::person_targets(state); - all.extend(path::interactable_targets(state)); - let mut covered = Vec::new(); - if let Some(size) = state.map_size() { - for y in 0..size.height { - for x in 0..size.width { - // `all` seeds every tile: the live fact "no new ground for hours", - // which is the one thing that would have cleared the frontier mark. - let all = std::env::var("FLY_PROBE_SEED_STOOD").is_ok_and(|v| v == "all"); - if all || state.tile_visited(x, y) { - covered.push(Tile::new(x, y)); - } - } - } - } - (all, covered) - }) - }; - let (talked, targets, stood, pushed, frontiers) = macros.ledgers_mut(); - targets.clock(ms); - // The tile underfoot is ground the live session had stood on, or its first observe here - // would read it as new ground and clear the frontier mark being rebuilt. - stood.record(map, Tile::new(player.x, player.y)); - // `FLY_PROBE_SEED_STOOD=1`: every tile of this map the adapter's lifetime ledger has, as a - // session that had walked the town for an hour would have them. - if std::env::var("FLY_PROBE_SEED_STOOD").is_ok_and(|value| value == "1" || value == "all") { - for tile in &covered { - stood.record(map, *tile); - } - } - if let Ok(tiles) = std::env::var("FLY_PROBE_SEED_PUSHED") { - for pair in tiles.split(';').filter(|pair| !pair.is_empty()) { - let mut xy = pair.split(',').map(|n| n.trim().parse::().expect("x,y")); - let (x, y) = (xy.next().expect("x"), xy.next().expect("y")); - pushed.record(map, Tile::new(x, y)); - } - } - if std::env::var("FLY_PROBE_SEED_EXHAUSTED").is_ok_and(|value| value == "1") { - frontiers.record(map); - } - if std::env::var("FLY_PROBE_SEED_TALKED").is_ok_and(|value| value == "1") { - for (_, target) in &things { - talked.record(map, *target); - } - } - if let Ok(keys) = std::env::var("FLY_PROBE_SEED_BLOCKED") { - for key in keys.split(';').filter(|key| !key.is_empty()) { - let id = match key { - "north" => ExitId::Edge(Edge::North), - "south" => ExitId::Edge(Edge::South), - "east" => ExitId::Edge(Edge::East), - "west" => ExitId::Edge(Edge::West), - warp => ExitId::Warp(warp.trim_start_matches("warp").parse().expect("warpN")), - }; - targets.record_blocked(map, TargetKey::Exit(id)); - } - } - println!( - "\n- seeded: pushed {:?}, frontier marks {:?}, talked {} things", - pushed, - frontiers, - talked.len() - ); -} - /// `FLY_PROBE_CATCH=route`. The live trap was map 2, scene `overworld`, a pad of `GO ROUTE` alone, /// refused about 740 times per ten brain minutes for hours with no button pressed. The ledgers /// that dealt that pad are session state and a restore starts them empty, so this *earns* them: @@ -1434,7 +1349,7 @@ fn seed_ledgers( /// the frame the way the macros do -- ledgers included -- and says which list emptied why, and /// whether the route search can reach any of the one button's goals. /// -/// `FLY_PROBE_FRAMES` bounds the drive; `FLY_PROBE_SEED` changes the choices; `FLY_PROBE_PREFER` +/// `FLY_PROBE_FRAMES` bounds the drive; `FLY_PROBE_RNG` changes the choices; `FLY_PROBE_PREFER` /// (comma-separated names) presses those buttons whenever they are dealt. /// /// [`PokemonPalette`]: flybrain_gb::pokemon_red::macros::PokemonPalette @@ -1445,7 +1360,7 @@ fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) use flybrain_gb::pokemon_red::macros::{PokemonPalette, palette, path}; let budget = env_usize("FLY_PROBE_FRAMES", 240_000); - let mut rng = env_usize("FLY_PROBE_SEED", 20_260_923) as u32 | 1; + let mut rng = env_usize("FLY_PROBE_RNG", 20_260_923) as u32 | 1; let hold_frames = 48usize; let trace_frames = env_usize("FLY_PROBE_TRACE_FRAMES", 0); // `FLY_PROBE_CATCH_AFTER=0` reads the frame at once, before any choice. @@ -1454,7 +1369,9 @@ fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) .map(|value| value.split(',').map(|name| name.trim().to_string()).collect()) .unwrap_or_default(); let mut macros = PokemonPalette::new(SEED); - seed_ledgers(&mut macros, gb, adapter, *ms); + if let Some(seeded) = ledgers::seed(&mut macros, gb, adapter, *ms, "FLY_PROBE_SEED") { + println!("\n- seeded: {seeded}"); + } let mut running = false; let mut since_decision = hold_frames; let mut last_pad = String::new(); diff --git a/services/flysim/crates/flysim/examples/support/ledgers.rs b/services/flysim/crates/flysim/examples/support/ledgers.rs new file mode 100644 index 0000000..a9e3c39 --- /dev/null +++ b/services/flysim/crates/flysim/examples/support/ledgers.rs @@ -0,0 +1,95 @@ +//! Rebuild a live session's macro ledgers from the environment, because a restore starts them +//! empty (`docs/design/macros.md` section 12: the ledgers are session state and never reach the +//! checkpoint). Shared by `scene_probe` and `trap_hunt`; `infra/docs/macros-traps.md` row 57 is +//! the trap that needed it: the pad that stalled was dealt by ledgers no checkpoint carries. +//! +//! Every variable is `_`, so the two examples keep their own namespaces: +//! +//! | name | meaning | +//! | --- | --- | +//! | `PUSHED` | `"x,y;x,y"`: tiles of the loaded map walled the way a scripted push-back walls them | +//! | `EXHAUSTED` | `1`: the loaded map's frontier marked unreachable (section 12.14) | +//! | `TALKED` | `1`: every person and sign on the loaded map written into the talked ledger | +//! | `BLOCKED` | `"warp2;east"`: those exits rested for the blocked window | +//! | `STOOD` | `1`: the adapter's covered ground; `all`: every tile of the map (no new ground at all) | +//! +//! The tile underfoot is always recorded as stood on, or the first observe would read it as new +//! ground and clear the frontier mark being rebuilt. Nothing is seeded when no variable is set. + +use flybrain_gb::pokemon_red::macros::PokemonPalette; +use flybrain_gb::pokemon_red::macros::cartridge::{Edge, ExitId, MacroState, TargetKey}; +use flybrain_gb::pokemon_red::macros::{Tile, path}; +use flybrain_gb::pokemon_red::{PokemonRedReward, state}; +use flybrain_gb::{AdapterLedger, Emulator, MacroPalette}; + +/// Seed `macros` from `_*` at brain millisecond `ms`, and say what was seeded. +pub fn seed( + macros: &mut PokemonPalette, + gb: &mut Emulator, + adapter: &PokemonRedReward, + ms: f64, + prefix: &str, +) -> Option { + let var = |name: &str| std::env::var(format!("{prefix}_{name}")).ok(); + let names = ["PUSHED", "EXHAUSTED", "TALKED", "BLOCKED", "STOOD"]; + if names.iter().all(|name| var(name).is_none()) { + return None; + } + let player = state::player(gb)?; + let map = player.map; + let stood_mode = var("STOOD").unwrap_or_default(); + macros.clock(ms); + let (things, covered): (Vec<_>, Vec) = + macros.inspect(gb, &AdapterLedger(adapter), |state: &mut dyn MacroState| { + let mut all = path::person_targets(state); + all.extend(path::interactable_targets(state)); + let mut covered = Vec::new(); + if let Some(size) = state.map_size() { + for y in 0..size.height { + for x in 0..size.width { + if stood_mode == "all" || state.tile_visited(x, y) { + covered.push(Tile::new(x, y)); + } + } + } + } + (all, covered) + }); + let (talked, targets, stood, pushed, frontiers) = macros.ledgers_mut(); + targets.clock(ms); + stood.record(map, Tile::new(player.x, player.y)); + if stood_mode == "1" || stood_mode == "all" { + for tile in &covered { + stood.record(map, *tile); + } + } + for pair in var("PUSHED").unwrap_or_default().split(';').filter(|pair| !pair.is_empty()) { + let mut xy = pair.split(',').map(|n| n.trim().parse::().expect("PUSHED is x,y;x,y")); + let (x, y) = (xy.next().expect("an x"), xy.next().expect("a y")); + pushed.record(map, Tile::new(x, y)); + } + if var("EXHAUSTED").as_deref() == Some("1") { + frontiers.record(map); + } + if var("TALKED").as_deref() == Some("1") { + for (_, target) in &things { + talked.record(map, *target); + } + } + for key in var("BLOCKED").unwrap_or_default().split(';').filter(|key| !key.is_empty()) { + let id = match key { + "north" => ExitId::Edge(Edge::North), + "south" => ExitId::Edge(Edge::South), + "east" => ExitId::Edge(Edge::East), + "west" => ExitId::Edge(Edge::West), + warp => ExitId::Warp(warp.trim_start_matches("warp").parse().expect("BLOCKED warpN")), + }; + targets.record_blocked(map, TargetKey::Exit(id)); + } + Some(format!( + "map {map:#04x}: pushed {pushed:?}, frontier marks {frontiers:?}, {} things talked to, \ + {} tiles stood on", + talked.len(), + stood.len() + )) +} diff --git a/services/flysim/crates/flysim/examples/trap_hunt.rs b/services/flysim/crates/flysim/examples/trap_hunt.rs index e202e8b..f3729f6 100644 --- a/services/flysim/crates/flysim/examples/trap_hunt.rs +++ b/services/flysim/crates/flysim/examples/trap_hunt.rs @@ -35,6 +35,7 @@ //! | `FLY_MACRO_BRAIN` | `FLY_DATASET`, else `data/fafb-v783` | the connectome | //! | `FLY_TRAP_THREADS` | 4 | sweep threads | //! | `FLY_TRAP_SEED` | 20260917 | seeds the palette | +//! | `FLY_TRAP_SEED_*` | unset | rebuilds session ledgers a restore starts empty: `PUSHED`, `EXHAUSTED`, `TALKED`, `BLOCKED`, `STOOD` (`examples/support/ledgers.rs`, row 57) | //! //! The frame order is `simloop.rs`'s, as `examples/palette_bench.rs` expresses it, so what this //! measures is the loop that ships rather than a second implementation of it. Without a @@ -58,6 +59,9 @@ use flysim::config::Config; use flysim::macros::{MacroLayer, macro_layer}; use flysim::snapshot::MacroMode; +#[path = "support/ledgers.rs"] +mod ledgers; + /// One brain minute in milliseconds. const MINUTE_MS: f64 = 60_000.0; @@ -186,6 +190,13 @@ struct Trace { recoveries: u64, rungs: Vec<(u32, &'static str, f64)>, outcomes: BTreeMap<&'static str, u64>, + /// What `FLY_TRAP_SEED_*` rebuilt, when anything (row 57). + seeded: Option, + /// `refused` outcomes by macro, and the run of one macro refused with the fly on one tile: + /// row 57's pad was one button refused 740 times running. + refusals: BTreeMap<&'static str, u64>, + refusal_run: (Option<(&'static str, Option<(u32, u32, u32)>)>, u64), + longest_refusal_run: (u64, &'static str), /// Frames spent in each scene, so a window full of macros can be read back to the scene that /// dealt them. scenes: BTreeMap<&'static str, u64>, @@ -352,6 +363,22 @@ fn run( config.loop_.game = "pokemon-red".to_string(); config.macros.mode = mode; let mut macros: Option = macro_layer(&config, hold_ms, seed); + let mut seeded_note: Option = None; + // Row 57: a trap dealt by session ledgers does not come back from a checkpoint, because a + // restore starts them empty. `FLY_TRAP_SEED_*` rebuilds them on the real palette, so both arms + // of a hunt start inside the state the live session was in. + if let Some(flavour) = config.macros.mode.palette_mode() { + let mut palette = + flybrain_gb::pokemon_red::macros::PokemonPalette::with_mode(seed, flavour); + let ms = agent.network.ms; + if let Some(seeded) = + ledgers::seed(&mut palette, &mut emulator, &adapter, ms, "FLY_TRAP_SEED") + { + eprintln!("seeded the session ledgers: {seeded}"); + seeded_note = Some(seeded); + macros = Some(MacroLayer::new(Box::new(palette), hold_ms)); + } + } let began_ms = agent.network.ms; let until = began_ms + minutes * MINUTE_MS; @@ -394,6 +421,10 @@ fn run( battle_starts: BTreeMap::new(), battle_pads: BTreeMap::new(), wall_seconds: 0.0, + seeded: seeded_note, + refusals: BTreeMap::new(), + refusal_run: (None, 0), + longest_refusal_run: (0, ""), }; if let Some(layer) = macros.as_mut() { @@ -483,6 +514,18 @@ fn run( } Some(outcome) => { *trace.outcomes.entry(outcome.as_str()).or_insert(0) += 1; + if outcome.as_str() == "refused" { + *trace.refusals.entry(event.name).or_insert(0) += 1; + let key = Some((event.name, location)); + trace.refusal_run = if trace.refusal_run.0 == key { + (key, trace.refusal_run.1 + 1) + } else { + (key, 1) + }; + if trace.refusal_run.1 > trace.longest_refusal_run.0 { + trace.longest_refusal_run = (trace.refusal_run.1, event.name); + } + } if let Some(run) = running.take() { let net = match (run.from, location) { (Some((map, x, y)), Some((at, ax, ay))) if map == at => { @@ -847,6 +890,9 @@ fn main() { trace.wall_seconds, Path::new(&checkpoint_path).display() ); + if let Some(seeded) = &trace.seeded { + println!("Session ledgers rebuilt before the first frame (`FLY_TRAP_SEED_*`): {seeded}.\n"); + } println!("| measure | value |"); println!("| --- | ---: |"); println!("| rung reached | {} |", trace.rungs.iter().map(|(rank, ..)| *rank).max().unwrap_or(0)); @@ -856,6 +902,15 @@ fn main() { for (outcome, count) in &trace.outcomes { println!("| {outcome} | {count} |"); } + if !trace.refusals.is_empty() { + let by: Vec = + trace.refusals.iter().map(|(name, count)| format!("`{name}` {count}")).collect(); + println!("| refused, by macro | {} |", by.join(", ")); + println!( + "| longest run of one macro refused on one tile | {} (`{}`) |", + trace.longest_refusal_run.0, trace.longest_refusal_run.1 + ); + } println!("| recoveries | {} |", trace.recoveries); println!("| windows examined | {windows} |"); println!("| windows flagged | {} |", traps.len());