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.
This commit is contained in:
parent
b8a5c8fc52
commit
2d04abaf52
3 changed files with 158 additions and 91 deletions
|
|
@ -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<Tile>) = {
|
||||
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::<u8>().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();
|
||||
|
|
|
|||
95
services/flysim/crates/flysim/examples/support/ledgers.rs
Normal file
95
services/flysim/crates/flysim/examples/support/ledgers.rs
Normal file
|
|
@ -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 `<PREFIX>_<NAME>`, 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 `<prefix>_*` 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<String> {
|
||||
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<Tile>) =
|
||||
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::<u8>().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()
|
||||
))
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
/// `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<MacroLayer> = macro_layer(&config, hold_ms, seed);
|
||||
let mut seeded_note: Option<String> = 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<String> =
|
||||
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());
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue