flysim: every harness runs the legacy frame: the trap hunt, the benches, the ROM tests' and the probe's stub drivers
trap_hunt, palette_bench and room_escape ran the frame through NeuralAgent::tick, which installs a frame and its rewards after the next ticks: one frame behind the stream, with the ratchet observed without the objective signal and a rollback that never re-observed the scene. They now restore the way the stream restores and run LegacyFrame::transition and ::boundary, looking in through FrameObserver; FLY_TRACE works in each of them. The stub-readout drivers of rom_macros_mode, rom_catch and scene_probe run LegacyFrame::execute and ::stub_advance, which are the same calls they made, in the same order.
This commit is contained in:
parent
ad1c0e3693
commit
60f09b79a3
8 changed files with 535 additions and 731 deletions
|
|
@ -259,10 +259,10 @@ impl NeuralAgent {
|
|||
/// scene has put on the pad (`docs/design/macros.md` section 12). A host with no macro group
|
||||
/// -- every caller that came before it -- passes `None` and decodes exactly as it always did.
|
||||
///
|
||||
/// `flysim`'s sim loop does not come through here (it drives the network and the decoder
|
||||
/// itself, so that the macro layer can read the emulator between the two), but the bench that
|
||||
/// measures the two arms against each other does, and a bench whose macro group could win a
|
||||
/// channel the scene never bound would be measuring something the stream cannot do.
|
||||
/// `flysim` does not come through here: its frame (`flysim::frame::LegacyFrame`) drives the
|
||||
/// network and the decoder itself, so that the macro layer can read the emulator between the
|
||||
/// two, and it installs a frame and its rewards straight after the frame rather than after the
|
||||
/// next ticks. Every `flysim` harness runs that frame too.
|
||||
pub fn tick_bound(
|
||||
&mut self,
|
||||
frame: &[u8],
|
||||
|
|
|
|||
|
|
@ -70,19 +70,20 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
|
||||
use flybrain_core::agent::{
|
||||
AgentConfig, NeuralAgent, RewardEvent as NeuralReward, TickOptions,
|
||||
AgentConfig, NeuralAgent,
|
||||
};
|
||||
use flybrain_core::dataset::load_brain_dataset_from_dir;
|
||||
use flybrain_core::decoder::gameboy::{gameboy_decoder_config_with_macros, to_button_mask};
|
||||
use flybrain_core::decoder::gameboy::gameboy_decoder_config_with_macros;
|
||||
use flybrain_core::lif::SweepPlan;
|
||||
use flybrain_gb::adapter::GameAdapter;
|
||||
use flybrain_gb::pokemon_red::PokemonRedReward;
|
||||
use flybrain_gb::ratchet::Ratchet;
|
||||
use flybrain_gb::recovery::{NeuralRecovery, recover_game};
|
||||
use flybrain_gb::{AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, buttons};
|
||||
use flysim::config::Config;
|
||||
use flysim::frame::{Executed, FrameObserver, LegacyFrame, Parts};
|
||||
use flysim::macros::{MacroLayer, OutcomeCounts, Silence, macro_layer};
|
||||
use flysim::snapshot::MacroMode;
|
||||
use flysim::trace::FrameTrace;
|
||||
|
||||
/// `constants/map_constants.asm`: Red's bedroom, where a cold boot ends up.
|
||||
const REDS_HOUSE_2F: u32 = 0x26;
|
||||
|
|
@ -103,25 +104,12 @@ fn emulator(rom: &[u8]) -> Emulator {
|
|||
.expect("binjgb should accept the cartridge")
|
||||
}
|
||||
|
||||
/// The neural half of a ratchet recovery, exactly as `simloop.rs` wires it.
|
||||
struct AgentRecovery<'a> {
|
||||
agent: &'a mut NeuralAgent,
|
||||
}
|
||||
/// Whether a macro owned the buttons of a frame: read as the executor hands the mask over.
|
||||
struct MacroOwned(bool);
|
||||
|
||||
impl NeuralRecovery for AgentRecovery<'_> {
|
||||
fn clear_decoder_holds(&mut self) {
|
||||
let ms = self.agent.network.ms;
|
||||
self.agent.decoder.clear_holds(ms);
|
||||
}
|
||||
|
||||
fn clear_eligibility(&mut self) {
|
||||
let ms = self.agent.network.ms;
|
||||
self.agent.network.plasticity.clear_eligibility(ms);
|
||||
}
|
||||
|
||||
fn set_visual_frame(&mut self, frame: &[u8]) {
|
||||
let (width, height) = (self.agent.frame.width, self.agent.frame.height);
|
||||
self.agent.network.set_visual_frame(frame, width, height);
|
||||
impl FrameObserver for MacroOwned {
|
||||
fn executed(&mut self, _frame: &LegacyFrame, parts: &mut Parts<'_>, _executed: &Executed) {
|
||||
self.0 = parts.macros.as_deref().is_some_and(|layer| layer.running().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,11 +232,9 @@ fn boot_to_bedroom(rom: &[u8]) -> Vec<u8> {
|
|||
|
||||
/// One arm: `hours` brain hours of the sim loop's frame order, unthrottled.
|
||||
///
|
||||
/// The order is `simloop.rs`'s (steps 2 to 10), as `NeuralAgent::tick` expresses it: the frame and
|
||||
/// the payouts handed to a tick are the ones the previous tick's buttons produced. The macro layer
|
||||
/// is consulted at exactly the two points the loop consults it — after the decode, before the
|
||||
/// buttons reach the emulator, and after the frame and its payouts — so the arm measures the
|
||||
/// wiring under test rather than a second implementation of it.
|
||||
/// The frame is `flysim::frame::LegacyFrame`, the one the stream runs, so the arm measures the
|
||||
/// wiring under test rather than a second implementation of it. (Before FND-01 the arms ticked the
|
||||
/// brain through `NeuralAgent::tick`, one frame behind the stream's order.)
|
||||
fn run_arm(
|
||||
rom: &[u8],
|
||||
data: &Arc<flybrain_core::dataset::BrainDataset>,
|
||||
|
|
@ -270,48 +256,39 @@ fn run_arm(
|
|||
.or(preset.exclusive.as_ref())
|
||||
.expect("the preset has a group")
|
||||
.hold_ms;
|
||||
let blocked_ms = preset.exclusive.as_ref().expect("the preset has an exclusive group").blocked_ms;
|
||||
let mut agent_config = AgentConfig::with_decoder(preset);
|
||||
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
||||
let mut arm = Arm { mode: mode.as_str(), ..Arm::default() };
|
||||
|
||||
match start {
|
||||
Start::Fresh { state, warmup_ms } => {
|
||||
emulator.import_state(state).expect("the booted state should import");
|
||||
if let Start::Fresh { warmup_ms, .. } = start {
|
||||
agent_config.warmup_ms = *warmup_ms;
|
||||
}
|
||||
Start::Live { checkpoint } => {
|
||||
emulator
|
||||
.import_state(&checkpoint.runtime.emulator)
|
||||
.expect("the checkpoint's emulator state should import");
|
||||
adapter
|
||||
.import_state(&checkpoint.runtime.reward)
|
||||
.expect("the checkpoint's reward ledger should import");
|
||||
let snapshot = (!checkpoint.runtime.ratchet_game.is_empty()).then(|| {
|
||||
flybrain_gb::ratchet::Snapshot {
|
||||
game: checkpoint.runtime.ratchet_game.clone(),
|
||||
frame: checkpoint.runtime.ratchet_frame.clone(),
|
||||
}
|
||||
});
|
||||
ratchet
|
||||
.import(Some(checkpoint.runtime.ratchet), snapshot, adapter.rank_ladder().len())
|
||||
.expect("the checkpoint's ratchet state should import");
|
||||
}
|
||||
}
|
||||
|
||||
let mut agent = NeuralAgent::new(Arc::clone(data), agent_config).expect("a valid agent");
|
||||
if threads > 1 {
|
||||
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
||||
}
|
||||
let mut frame = LegacyFrame::new()
|
||||
.with_trace(FrameTrace::from_env().expect("FLY_TRACE should name a writable file"));
|
||||
match start {
|
||||
Start::Fresh { .. } => {
|
||||
agent.warmup(Some(emulator.framebuffer())).expect("warm-up");
|
||||
}
|
||||
Start::Live { checkpoint } => {
|
||||
agent.import_state(&checkpoint.agent).expect("the checkpoint's agent should import");
|
||||
let (width, height) = (agent.frame.width, agent.frame.height);
|
||||
agent.network.set_visual_frame(&checkpoint.runtime.framebuffer, width, height);
|
||||
Start::Fresh { state, warmup_ms: _ } => {
|
||||
emulator.import_state(state).expect("the booted state should import");
|
||||
frame.frame_buffer.copy_from_slice(emulator.framebuffer());
|
||||
agent.warmup(Some(&frame.frame_buffer)).expect("warm-up");
|
||||
}
|
||||
// The stream's own restore, into the stream's own frame.
|
||||
Start::Live { checkpoint } => frame
|
||||
.restore(
|
||||
&mut Parts {
|
||||
agent: &mut agent,
|
||||
emulator: &mut emulator,
|
||||
adapter: &mut adapter,
|
||||
ratchet: &mut ratchet,
|
||||
macros: None,
|
||||
},
|
||||
checkpoint,
|
||||
)
|
||||
.expect("the checkpoint should restore"),
|
||||
}
|
||||
|
||||
// The layer under test, built the way the sim loop builds it: from the configuration, so raw
|
||||
|
|
@ -328,136 +305,62 @@ fn run_arm(
|
|||
|
||||
let began_ms = agent.network.ms;
|
||||
let until = began_ms + hours * HOUR_MS;
|
||||
let mut frame = emulator.framebuffer().to_vec();
|
||||
let mut payouts: Vec<flybrain_gb::RewardEvent> = Vec::new();
|
||||
let mut location = adapter.location();
|
||||
let mut blocked_since_ms = began_ms;
|
||||
let mut held_channel: Option<String> = agent.decoder.current().map(str::to_string);
|
||||
let mut rank = adapter.progress().rank;
|
||||
let tiles_at_start = adapter.progress().unique_locations;
|
||||
arm.rungs.push((rank, adapter.progress().rank_label, 0.0));
|
||||
|
||||
// The scene has not been observed yet, so the first frame is decided on an empty palette,
|
||||
// which presses nothing. That is one frame, and it is the honest starting state.
|
||||
// One observation before the first frame, as the sim loop takes after a restore, so frame one
|
||||
// is decided on a real palette.
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
||||
}
|
||||
|
||||
while agent.network.ms < until {
|
||||
let rewards: Vec<NeuralReward> = payouts
|
||||
.iter()
|
||||
.map(|event| {
|
||||
NeuralReward::with_stimulation(event.value, f64::from(event.stimulation_ms))
|
||||
})
|
||||
.collect();
|
||||
let options = TickOptions { rewards: &rewards, boot: adapter.boot(), learn: true };
|
||||
|
||||
// The blocked-direction cooldown's input, as `simloop.rs` computes it.
|
||||
let ms = agent.network.ms;
|
||||
let blocked = (blocked_ms > 0.0 && ms - blocked_since_ms >= blocked_ms)
|
||||
.then(|| agent.decoder.current().map(str::to_string))
|
||||
.flatten();
|
||||
// The scene's own macro buttons, from the palette the previous frame's `observe` dealt:
|
||||
// the same mask the sim loop passes (`docs/design/macros.md` section 12). `None` in the
|
||||
// raw arm, which has no layer and no macro group at all.
|
||||
let bound = macros.as_ref().map(MacroLayer::bound_channels);
|
||||
let result = agent
|
||||
.tick_bound(&frame, &options, blocked.as_deref(), bound.as_deref())
|
||||
.expect("a tick");
|
||||
let held = agent.decoder.current().map(str::to_string);
|
||||
if held != held_channel {
|
||||
held_channel = held;
|
||||
blocked_since_ms = ms;
|
||||
}
|
||||
|
||||
// Step 4, with the layer in the middle of it in macros mode and absent in raw mode.
|
||||
let ms = agent.network.ms;
|
||||
let mut mask = to_button_mask(&result.active);
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
let decision = layer.decide(&result.active, mask, ms, &mut emulator, &ledger);
|
||||
mask = decision.mask;
|
||||
if let Some(silence) = decision.silence {
|
||||
let mut parts = Parts {
|
||||
agent: &mut agent,
|
||||
emulator: &mut emulator,
|
||||
adapter: &mut adapter,
|
||||
ratchet: &mut ratchet,
|
||||
macros: macros.as_mut(),
|
||||
};
|
||||
let mut owned = MacroOwned(false);
|
||||
let transition = frame.transition(&mut parts, &mut owned).expect("a frame");
|
||||
let ms = transition.ms;
|
||||
let executed = &transition.executed;
|
||||
if let Some(layer) = parts.macros.as_deref() {
|
||||
if let Some(silence) = executed.silence {
|
||||
*arm.silence.entry(silence.label()).or_insert(0) += 1;
|
||||
}
|
||||
for event in decision.events.iter().filter(|event| event.outcome.is_none()) {
|
||||
for event in executed.events.iter().filter(|event| event.outcome.is_none()) {
|
||||
*arm.by_rank.entry(event.slot).or_insert(0) += 1;
|
||||
}
|
||||
if layer.running().is_some() {
|
||||
if owned.0 {
|
||||
arm.macro_frames += 1;
|
||||
}
|
||||
*arm.scenes.entry(layer.scene_name()).or_insert(0) += 1;
|
||||
}
|
||||
if mask == 0 {
|
||||
if executed.mask == 0 {
|
||||
arm.idle_frames += 1;
|
||||
}
|
||||
emulator.set_buttons(mask as u8);
|
||||
emulator.run_frame().expect("a frame should complete");
|
||||
arm.frames += 1;
|
||||
frame.copy_from_slice(emulator.framebuffer());
|
||||
|
||||
// Steps 7 to 9, then the scene.
|
||||
payouts = adapter.sample(&mut emulator, ms);
|
||||
for event in &payouts {
|
||||
for event in &transition.evaluated.rewards {
|
||||
arm.reward += event.value;
|
||||
*arm.payouts.entry(event.kind).or_insert(0) += 1;
|
||||
}
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
||||
*arm.scenes.entry(layer.scene_name()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
let now = adapter.location();
|
||||
if now.is_some() && now != location {
|
||||
location = now;
|
||||
blocked_since_ms = ms;
|
||||
}
|
||||
arm.digest = hash(arm.digest, u64::from(mask));
|
||||
if let Some((map, x, y)) = location {
|
||||
arm.digest = hash(arm.digest, u64::from(executed.mask));
|
||||
if let Some((map, x, y)) = frame.location {
|
||||
arm.digest = hash(arm.digest, u64::from(map) << 32 | u64::from(x) << 16 | u64::from(y));
|
||||
}
|
||||
|
||||
// Step 10: the ratchet, with the adapter's own policy.
|
||||
let progress = adapter.progress();
|
||||
let progress = transition.evaluated.progress;
|
||||
if progress.rank != rank {
|
||||
rank = progress.rank;
|
||||
arm.rungs.push((rank, progress.rank_label, ms - began_ms));
|
||||
}
|
||||
let safe = adapter.safe_for_snapshot();
|
||||
let capture_due = safe && u64::from(progress.rank) > ratchet.state.best;
|
||||
let captured = capture_due.then(|| flybrain_gb::ratchet::Snapshot {
|
||||
game: emulator.export_state().expect("state export"),
|
||||
frame: frame.clone(),
|
||||
});
|
||||
let recover = ratchet.observe_with_game_over(
|
||||
safe,
|
||||
u64::from(progress.rank),
|
||||
progress.unique_locations as u64,
|
||||
ms as u64,
|
||||
adapter.game_over(),
|
||||
|| captured.expect("the ratchet only captures when a snapshot was prepared"),
|
||||
);
|
||||
if recover {
|
||||
let snapshot = flybrain_gb::ratchet::Snapshot {
|
||||
game: ratchet.game().expect("a recovery has a snapshot").to_vec(),
|
||||
frame: ratchet.frame().expect("a recovery has a framebuffer").to_vec(),
|
||||
};
|
||||
let restored = {
|
||||
let mut neural = AgentRecovery { agent: &mut agent };
|
||||
recover_game(&mut emulator, &mut adapter, &mut neural, &snapshot)
|
||||
.expect("recovering the game")
|
||||
};
|
||||
frame.copy_from_slice(&restored);
|
||||
emulator.set_buttons(0);
|
||||
let boundary = frame.boundary(&mut parts, &progress, ms).expect("the boundary");
|
||||
if boundary.rollback.is_some() {
|
||||
arm.recoveries += 1;
|
||||
location = adapter.location();
|
||||
held_channel = None;
|
||||
blocked_since_ms = ms;
|
||||
// The sim loop abandons a running macro on a rollback, and so does this.
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
layer.cancel(ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ use std::sync::Arc;
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use flybrain_core::agent::{
|
||||
AgentConfig, GAMEBOY_MS_PER_FRAME, NeuralAgent, RewardEvent as NeuralReward, TickOptions,
|
||||
AgentConfig, GAMEBOY_MS_PER_FRAME, NeuralAgent,
|
||||
};
|
||||
use flybrain_core::dataset::load_brain_dataset_from_dir;
|
||||
use flybrain_core::decoder::gameboy::{GAMEBOY_BUTTONS, gameboy_decoder_config, to_button_mask};
|
||||
|
|
@ -88,8 +88,8 @@ use flybrain_gb::adapter::{GameAdapter, MemoryReader};
|
|||
use flybrain_gb::pokemon_red::symbols::ram;
|
||||
use flybrain_gb::pokemon_red::{PokemonRedReward, SUPPORTED_ROM};
|
||||
use flybrain_gb::ratchet::Ratchet;
|
||||
use flybrain_gb::recovery::{NeuralRecovery, recover_game};
|
||||
use flybrain_gb::{DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, buttons};
|
||||
use flysim::frame::{FrameObserver, FramePhase, LegacyFrame, Parts};
|
||||
|
||||
/// `constants/map_constants.asm`: Red's bedroom and the ground floor of his house.
|
||||
const REDS_HOUSE_2F: u32 = 0x26;
|
||||
|
|
@ -496,28 +496,6 @@ fn print_table(title: &str, cells: &BTreeMap<(String, u64, u64, u64), Cell>, roo
|
|||
// The real brain
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
/// The neural half of a ratchet recovery, exactly as `flysim::simloop` wires it.
|
||||
struct AgentRecovery<'a> {
|
||||
agent: &'a mut NeuralAgent,
|
||||
}
|
||||
|
||||
impl NeuralRecovery for AgentRecovery<'_> {
|
||||
fn clear_decoder_holds(&mut self) {
|
||||
let ms = self.agent.network.ms;
|
||||
self.agent.decoder.clear_holds(ms);
|
||||
}
|
||||
|
||||
fn clear_eligibility(&mut self) {
|
||||
let ms = self.agent.network.ms;
|
||||
self.agent.network.plasticity.clear_eligibility(ms);
|
||||
}
|
||||
|
||||
fn set_visual_frame(&mut self, frame: &[u8]) {
|
||||
let (width, height) = (self.agent.frame.width, self.agent.frame.height);
|
||||
self.agent.network.set_visual_frame(frame, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/// Where one brain run starts.
|
||||
enum Start<'a> {
|
||||
/// A save state and the map it stands in: a fresh fly, warmed up here.
|
||||
|
|
@ -666,6 +644,97 @@ fn survey(rom: &[u8], state: &[u8]) -> Survey {
|
|||
(reachable, exits)
|
||||
}
|
||||
|
||||
/// The room-escape instrumentation inside the stream's frame: the decoder as it stood before
|
||||
/// the decode and after it, read at the one point between the two.
|
||||
struct Escape<'a> {
|
||||
/// The readout before this frame's decode.
|
||||
before: Option<flybrain_core::decoder::DecoderState>,
|
||||
hold_start: (Option<(u32, u32, u32)>, f64),
|
||||
run_winner: Option<String>,
|
||||
run_length: f64,
|
||||
start_map: u32,
|
||||
exits: &'a BTreeMap<(u32, u32), Vec<&'static str>>,
|
||||
hold_ms: f64,
|
||||
trace: Trace,
|
||||
}
|
||||
|
||||
impl FrameObserver for Escape<'_> {
|
||||
fn after(&mut self, phase: FramePhase, agent: &mut NeuralAgent) {
|
||||
if phase == FramePhase::Ticked {
|
||||
self.before = Some(agent.decoder.export_state());
|
||||
}
|
||||
}
|
||||
|
||||
fn before_execute(&mut self, frame: &LegacyFrame, parts: &mut Parts<'_>, _active: &[String]) {
|
||||
let Some(before) = self.before.take() else { return };
|
||||
let agent = &*parts.agent;
|
||||
let after = agent.decoder.export_state();
|
||||
if after.next_decision == before.next_decision {
|
||||
return;
|
||||
}
|
||||
let ms = agent.network.ms;
|
||||
let location = frame.location;
|
||||
let trace = &mut self.trace;
|
||||
trace.decisions += 1;
|
||||
let winner = after.current.clone().expect("a decision names a winner");
|
||||
// The raw argmax, recomputed from the decoder's own inputs: the rates the decode saw, the
|
||||
// calibrated baseline, and the fatigue as it stood *before* the decision. Comparing it
|
||||
// with the winner is what "the incumbent won by hysteresis" means.
|
||||
let adjusted = |channel: &str| {
|
||||
let index = DIRECTIONS.iter().position(|name| *name == channel).expect("a direction");
|
||||
let role = ROLES[index];
|
||||
let rate = agent.network.rates.get_or_zero(role);
|
||||
let base = after.baseline.get_or_zero(role);
|
||||
(rate + 1.0) / (base + 1.0) / (1.0 + before.fatigue.get_or_zero(channel))
|
||||
};
|
||||
let mut argmax = DIRECTIONS[0];
|
||||
for channel in DIRECTIONS.iter().skip(1) {
|
||||
if adjusted(channel) > adjusted(argmax) {
|
||||
argmax = channel;
|
||||
}
|
||||
}
|
||||
if argmax != winner {
|
||||
trace.hysteresis_holds += 1;
|
||||
}
|
||||
if let Some(name) = DIRECTIONS.iter().find(|name| **name == winner) {
|
||||
*trace.wins.entry(name).or_insert(0) += 1;
|
||||
}
|
||||
if self.run_winner.as_deref() == Some(winner.as_str()) {
|
||||
self.run_length += 1.0;
|
||||
} else {
|
||||
if self.run_length > 0.0 {
|
||||
trace.runs.push(self.run_length);
|
||||
}
|
||||
self.run_winner = Some(winner.clone());
|
||||
self.run_length = 1.0;
|
||||
}
|
||||
|
||||
// Was the hold that just ended a wall bump? The location now against the location at the
|
||||
// previous decision, for the direction that was held in between.
|
||||
if let (Some(previous), Some(held)) = (self.hold_start.0, before.current.as_deref())
|
||||
&& ms - self.hold_start.1 >= self.hold_ms
|
||||
&& location == Some(previous)
|
||||
&& let Some(name) = DIRECTIONS.iter().find(|name| **name == held)
|
||||
{
|
||||
*trace.blocked_holds.entry(name).or_insert(0) += 1;
|
||||
}
|
||||
self.hold_start = (location, ms);
|
||||
|
||||
// The decision this whole exercise is about: standing on a tile one press from leaving,
|
||||
// did the readout choose that press? Only on the starting map: the bedroom has walkable
|
||||
// tiles at the same coordinates and they are not these exits.
|
||||
if let Some(leaving) = location
|
||||
.filter(|(map, _, _)| *map == self.start_map)
|
||||
.and_then(|(_, x, y)| self.exits.get(&(x, y)))
|
||||
{
|
||||
trace.exit_decisions += 1;
|
||||
if leaving.iter().any(|direction| *direction == winner) {
|
||||
trace.exit_decisions_taken += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One instrumented brain run: the real network, the real readout, the real adapter, and -- from a
|
||||
/// live checkpoint -- the real reward ledger and the real ratchet.
|
||||
///
|
||||
|
|
@ -697,174 +766,81 @@ fn brain_trace(
|
|||
let mut emulator = emulator(rom);
|
||||
let mut adapter = PokemonRedReward::new();
|
||||
let mut agent_config = AgentConfig::with_decoder(config.clone());
|
||||
let (hold_ms, blocked_ms) = {
|
||||
let group = config.exclusive.as_ref().expect("the Game Boy preset has an exclusive group");
|
||||
(group.hold_ms, group.blocked_ms)
|
||||
};
|
||||
let hold_ms =
|
||||
config.exclusive.as_ref().expect("the Game Boy preset has an exclusive group").hold_ms;
|
||||
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
||||
let mut trace = Trace::default();
|
||||
|
||||
let start_map = match &start {
|
||||
Start::Fresh { state, map, warmup_ms } => {
|
||||
emulator.import_state(state).expect("the starting state should import");
|
||||
if let Start::Fresh { warmup_ms, .. } = &start {
|
||||
agent_config.warmup_ms = *warmup_ms;
|
||||
*map
|
||||
}
|
||||
Start::Live { checkpoint, .. } => {
|
||||
emulator
|
||||
.import_state(&checkpoint.runtime.emulator)
|
||||
.expect("the checkpoint's emulator state should import");
|
||||
adapter
|
||||
.import_state(&checkpoint.runtime.reward)
|
||||
.expect("the checkpoint's reward ledger should import");
|
||||
let snapshot = (!checkpoint.runtime.ratchet_game.is_empty()).then(|| {
|
||||
flybrain_gb::ratchet::Snapshot {
|
||||
game: checkpoint.runtime.ratchet_game.clone(),
|
||||
frame: checkpoint.runtime.ratchet_frame.clone(),
|
||||
}
|
||||
});
|
||||
ratchet
|
||||
.import(Some(checkpoint.runtime.ratchet), snapshot, adapter.rank_ladder().len())
|
||||
.expect("the checkpoint's ratchet state should import");
|
||||
u32::from(emulator.read8(ram::wCurMap))
|
||||
}
|
||||
};
|
||||
|
||||
let mut agent = NeuralAgent::new(Arc::clone(data), agent_config).expect("a valid agent");
|
||||
if threads > 1 {
|
||||
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
||||
}
|
||||
match &start {
|
||||
Start::Fresh { .. } => agent.warmup(Some(emulator.framebuffer())).expect("warm-up"),
|
||||
// The stream's own frame (`flysim::frame::LegacyFrame`), in raw mode: no macro layer.
|
||||
let mut frame = LegacyFrame::new();
|
||||
let start_map = match &start {
|
||||
Start::Fresh { state, map, .. } => {
|
||||
emulator.import_state(state).expect("the starting state should import");
|
||||
frame.frame_buffer.copy_from_slice(emulator.framebuffer());
|
||||
agent.warmup(Some(&frame.frame_buffer)).expect("warm-up");
|
||||
*map
|
||||
}
|
||||
Start::Live { checkpoint, rng } => {
|
||||
let mut state = checkpoint.agent.clone();
|
||||
state.network.rng = *rng;
|
||||
agent.import_state(&state).expect("the checkpoint's agent state should import");
|
||||
let (width, height) = (agent.frame.width, agent.frame.height);
|
||||
agent.network.set_visual_frame(&checkpoint.runtime.framebuffer, width, height);
|
||||
}
|
||||
let mut checkpoint = (*checkpoint).clone();
|
||||
checkpoint.agent.network.rng = *rng;
|
||||
frame
|
||||
.restore(
|
||||
&mut Parts {
|
||||
agent: &mut agent,
|
||||
emulator: &mut emulator,
|
||||
adapter: &mut adapter,
|
||||
ratchet: &mut ratchet,
|
||||
macros: None,
|
||||
},
|
||||
&checkpoint,
|
||||
)
|
||||
.expect("the checkpoint should restore");
|
||||
u32::from(emulator.read8(ram::wCurMap))
|
||||
}
|
||||
};
|
||||
|
||||
trace.maps.push(start_map);
|
||||
let began_ms = agent.network.ms;
|
||||
let until = began_ms + minutes * 60_000.0;
|
||||
// The sim loop's own order (`simloop.rs`, steps 2 to 10), as `NeuralAgent::tick` expresses it:
|
||||
// the frame and the payouts handed to a tick are the ones the previous tick's buttons produced.
|
||||
let mut frame = emulator.framebuffer().to_vec();
|
||||
let mut payouts: Vec<flybrain_gb::RewardEvent> = Vec::new();
|
||||
let mut location = adapter.location();
|
||||
// The blocked-direction cooldown's window, restarted by a move *or* by a new winner, exactly
|
||||
// as `simloop.rs` restarts it: a direction that has just won has not had a hold to move in yet.
|
||||
let mut blocked_since_ms = began_ms;
|
||||
let mut held_channel: Option<String> = agent.decoder.current().map(str::to_string);
|
||||
let mut hold_start = (location, began_ms);
|
||||
let mut run_winner: Option<String> = None;
|
||||
let mut run_length = 0.0f64;
|
||||
let mut escape = Escape {
|
||||
before: None,
|
||||
hold_start: (frame.location, began_ms),
|
||||
run_winner: None,
|
||||
run_length: 0.0,
|
||||
start_map,
|
||||
exits,
|
||||
hold_ms,
|
||||
trace,
|
||||
};
|
||||
// Reported on its own: the longest stretch with no movement at all, whatever was held.
|
||||
let mut still_since_ms = began_ms;
|
||||
let tiles_at_start = adapter.progress().unique_locations;
|
||||
trace.tiles = tiles_at_start;
|
||||
escape.trace.tiles = tiles_at_start;
|
||||
|
||||
while agent.network.ms < until {
|
||||
let rewards: Vec<NeuralReward> = payouts
|
||||
.iter()
|
||||
.map(|event| {
|
||||
NeuralReward::with_stimulation(event.value, f64::from(event.stimulation_ms))
|
||||
})
|
||||
.collect();
|
||||
let options = TickOptions { rewards: &rewards, boot: adapter.boot(), learn: true };
|
||||
|
||||
// The blocked-direction cooldown's input, computed the way `simloop.rs` computes it: the
|
||||
// channel the readout is holding, once the adapter's location has stood still for a whole
|
||||
// hold. `blocked_ms == 0` is the rule switched off, and reports nothing.
|
||||
let ms = agent.network.ms;
|
||||
let blocked = (blocked_ms > 0.0 && ms - blocked_since_ms >= blocked_ms)
|
||||
.then(|| agent.decoder.current().map(str::to_string))
|
||||
.flatten();
|
||||
|
||||
let before = agent.decoder.export_state();
|
||||
let result = agent.tick_blocked(&frame, &options, blocked.as_deref()).expect("a tick");
|
||||
let after = agent.decoder.export_state();
|
||||
let held = agent.decoder.current().map(str::to_string);
|
||||
if held != held_channel {
|
||||
held_channel = held;
|
||||
blocked_since_ms = ms;
|
||||
}
|
||||
|
||||
if after.next_decision != before.next_decision {
|
||||
trace.decisions += 1;
|
||||
let winner = after.current.clone().expect("a decision names a winner");
|
||||
// The raw argmax, recomputed from the decoder's own inputs: the rates the decode saw
|
||||
// (`tick` decodes on the post-step rates and nothing changes them afterwards), the
|
||||
// calibrated baseline, and the fatigue as it stood *before* the decision. Comparing it
|
||||
// with the winner is what "the incumbent won by hysteresis" means.
|
||||
let adjusted = |channel: &str| {
|
||||
let index =
|
||||
DIRECTIONS.iter().position(|name| *name == channel).expect("a direction");
|
||||
let role = ROLES[index];
|
||||
let rate = agent.network.rates.get_or_zero(role);
|
||||
let base = after.baseline.get_or_zero(role);
|
||||
(rate + 1.0) / (base + 1.0) / (1.0 + before.fatigue.get_or_zero(channel))
|
||||
let mut parts = Parts {
|
||||
agent: &mut agent,
|
||||
emulator: &mut emulator,
|
||||
adapter: &mut adapter,
|
||||
ratchet: &mut ratchet,
|
||||
macros: None,
|
||||
};
|
||||
let mut argmax = DIRECTIONS[0];
|
||||
for channel in DIRECTIONS.iter().skip(1) {
|
||||
if adjusted(channel) > adjusted(argmax) {
|
||||
argmax = channel;
|
||||
}
|
||||
}
|
||||
if argmax != winner {
|
||||
trace.hysteresis_holds += 1;
|
||||
}
|
||||
if let Some(name) = DIRECTIONS.iter().find(|name| **name == winner) {
|
||||
*trace.wins.entry(name).or_insert(0) += 1;
|
||||
}
|
||||
if run_winner.as_deref() == Some(winner.as_str()) {
|
||||
run_length += 1.0;
|
||||
} else {
|
||||
if run_length > 0.0 {
|
||||
trace.runs.push(run_length);
|
||||
}
|
||||
run_winner = Some(winner.clone());
|
||||
run_length = 1.0;
|
||||
}
|
||||
let location_before = frame.location;
|
||||
let transition = frame.transition(&mut parts, &mut escape).expect("a frame");
|
||||
let ms = transition.ms;
|
||||
let trace = &mut escape.trace;
|
||||
trace.reward += transition.evaluated.rewards.iter().map(|event| event.value).sum::<f64>();
|
||||
trace.tiles = transition.evaluated.progress.unique_locations;
|
||||
|
||||
// Was the hold that just ended a wall bump? The location now against the location at
|
||||
// the previous decision, for the direction that was held in between.
|
||||
if let (Some(previous), Some(held)) = (hold_start.0, before.current.as_deref())
|
||||
&& ms - hold_start.1 >= hold_ms
|
||||
&& location == Some(previous)
|
||||
&& let Some(name) = DIRECTIONS.iter().find(|name| **name == held)
|
||||
{
|
||||
*trace.blocked_holds.entry(name).or_insert(0) += 1;
|
||||
}
|
||||
hold_start = (location, ms);
|
||||
|
||||
// The decision this whole exercise is about: standing on a tile one press from
|
||||
// leaving, did the readout choose that press? Only on the starting map: the bedroom
|
||||
// has walkable tiles at the same coordinates and they are not these exits.
|
||||
if let Some(leaving) = location
|
||||
.filter(|(map, _, _)| *map == start_map)
|
||||
.and_then(|(_, x, y)| exits.get(&(x, y)))
|
||||
{
|
||||
trace.exit_decisions += 1;
|
||||
if leaving.iter().any(|direction| *direction == winner) {
|
||||
trace.exit_decisions_taken += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emulator.set_buttons(to_button_mask(&result.active) as u8);
|
||||
emulator.run_frame().expect("a frame should complete");
|
||||
frame.copy_from_slice(emulator.framebuffer());
|
||||
let ms = agent.network.ms;
|
||||
payouts = adapter.sample(&mut emulator, ms);
|
||||
trace.reward += payouts.iter().map(|event| event.value).sum::<f64>();
|
||||
trace.tiles = adapter.progress().unique_locations;
|
||||
|
||||
let now = adapter.location();
|
||||
if now.is_some() && now != location {
|
||||
location = now;
|
||||
blocked_since_ms = ms;
|
||||
let location = frame.location;
|
||||
if location != location_before {
|
||||
still_since_ms = ms;
|
||||
}
|
||||
trace.longest_still_ms = trace.longest_still_ms.max(ms - still_since_ms);
|
||||
|
|
@ -876,40 +852,15 @@ fn brain_trace(
|
|||
*trace.exit_tile_frames.entry(tile).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// Step 10: the ratchet, with the adapter's own policy and the checkpoint's own budget.
|
||||
let progress = adapter.progress();
|
||||
let safe = adapter.safe_for_snapshot();
|
||||
let capture_due = safe && u64::from(progress.rank) > ratchet.state.best;
|
||||
let captured = capture_due.then(|| flybrain_gb::ratchet::Snapshot {
|
||||
game: emulator.export_state().expect("state export"),
|
||||
frame: frame.clone(),
|
||||
});
|
||||
let recover = ratchet.observe(
|
||||
safe,
|
||||
u64::from(progress.rank),
|
||||
progress.unique_locations as u64,
|
||||
ms as u64,
|
||||
|| captured.expect("the ratchet only captures when a snapshot was prepared"),
|
||||
);
|
||||
if recover {
|
||||
let snapshot = flybrain_gb::ratchet::Snapshot {
|
||||
game: ratchet.game().expect("a recovery has a snapshot").to_vec(),
|
||||
frame: ratchet.frame().expect("a recovery has a framebuffer").to_vec(),
|
||||
};
|
||||
let restored = {
|
||||
let mut neural = AgentRecovery { agent: &mut agent };
|
||||
recover_game(&mut emulator, &mut adapter, &mut neural, &snapshot)
|
||||
.expect("recovering the game")
|
||||
};
|
||||
frame.copy_from_slice(&restored);
|
||||
emulator.set_buttons(0);
|
||||
trace.recoveries += 1;
|
||||
location = adapter.location();
|
||||
held_channel = None;
|
||||
blocked_since_ms = ms;
|
||||
// The ratchet, with the adapter's own policy and the checkpoint's own budget.
|
||||
let progress = transition.evaluated.progress;
|
||||
let boundary = frame.boundary(&mut parts, &progress, ms).expect("the boundary");
|
||||
if boundary.rollback.is_some() {
|
||||
escape.trace.recoveries += 1;
|
||||
still_since_ms = ms;
|
||||
hold_start = (location, ms);
|
||||
escape.hold_start = (frame.location, ms);
|
||||
}
|
||||
let trace = &mut escape.trace;
|
||||
|
||||
let Some(map) = adapter.map_id() else { continue };
|
||||
if trace.maps.last() != Some(&map) {
|
||||
|
|
@ -925,6 +876,7 @@ fn brain_trace(
|
|||
}
|
||||
}
|
||||
}
|
||||
let Escape { mut trace, run_length, .. } = escape;
|
||||
if run_length > 0.0 {
|
||||
trace.runs.push(run_length);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -808,6 +808,8 @@ fn accept_survey(
|
|||
// [box not drawn, box drawn] x [press refused, press honoured], over every frame whose cursor
|
||||
// bytes say "the move list" -- which is the whole of what the seam read before row 50.
|
||||
let mut readings = [[0usize; 2]; 2];
|
||||
// The stream's frame (`flysim::frame::LegacyFrame`), behind the stub readout.
|
||||
let mut legacy = flysim::frame::LegacyFrame::new();
|
||||
|
||||
println!("\n## Row 50: every battle frame, pressed at\n");
|
||||
println!("```");
|
||||
|
|
@ -823,18 +825,9 @@ fn accept_survey(
|
|||
}
|
||||
let bound = layer.bound_channels();
|
||||
let active = decoder.decode_bound(&rates(hot), *ms, false, None, Some(&bound));
|
||||
let mask = {
|
||||
let ledger = AdapterLedger(adapter);
|
||||
layer.decide(&active, 0, *ms, gb, &ledger).mask
|
||||
};
|
||||
gb.set_buttons(mask as u8);
|
||||
gb.run_frame().expect("a frame should complete");
|
||||
legacy.execute(Some(&mut *layer), &active, 0, *ms, gb, &*adapter);
|
||||
*ms += MS_PER_FRAME;
|
||||
adapter.sample(gb, *ms);
|
||||
{
|
||||
let ledger = AdapterLedger(adapter);
|
||||
let _ = layer.observe(gb, &ledger, *ms);
|
||||
}
|
||||
legacy.stub_advance(Some(&mut *layer), gb, adapter, *ms).expect("a frame should complete");
|
||||
|
||||
let Some((name, own_turn, forced)) = battle_reading(gb, adapter) else { continue };
|
||||
let geom = move_cursor_geometry(gb);
|
||||
|
|
@ -1801,6 +1794,8 @@ fn main() {
|
|||
let mut noattack = 0usize;
|
||||
let mut before = (0u8, 0u8, 0u8);
|
||||
let mut surveyed = 0usize;
|
||||
// The stream's frame (`flysim::frame::LegacyFrame`), behind the stub readout.
|
||||
let mut legacy = flysim::frame::LegacyFrame::new();
|
||||
for frame in 0..budget {
|
||||
let bursting = ms < next_burst + BURST_MS;
|
||||
let hot = bursting.then(|| channels[(burst / HOLDS_PER_SLOT) % channels.len()]);
|
||||
|
|
@ -1810,18 +1805,11 @@ fn main() {
|
|||
}
|
||||
let bound = layer.bound_channels();
|
||||
let active = decoder.decode_bound(&rates(hot), ms, false, None, Some(&bound));
|
||||
let mask = {
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
layer.decide(&active, 0, ms, &mut gb, &ledger).mask
|
||||
};
|
||||
gb.set_buttons(mask as u8);
|
||||
gb.run_frame().expect("a frame should complete");
|
||||
legacy.execute(Some(&mut layer), &active, 0, ms, &mut gb, &adapter);
|
||||
ms += MS_PER_FRAME;
|
||||
adapter.sample(&mut gb, ms);
|
||||
{
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
let _ = layer.observe(&mut gb, &ledger, ms);
|
||||
}
|
||||
legacy
|
||||
.stub_advance(Some(&mut layer), &mut gb, &mut adapter, ms)
|
||||
.expect("a frame should complete");
|
||||
if catch_script
|
||||
&& gb.read8(ram::wSimulatedJoypadStatesIndex) != 0
|
||||
&& gb.read8(ram::wCurMap) == 1
|
||||
|
|
|
|||
|
|
@ -36,9 +36,11 @@
|
|||
//! | `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) |
|
||||
//! | `FLY_TRACE` | unset | a path: the stream's per-frame trace of this run (`flysim::trace`), comparable with the service's own |
|
||||
//!
|
||||
//! 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
|
||||
//! The frame is `flysim::frame::LegacyFrame`, the one the stream runs, restored the way the stream
|
||||
//! restores it, so what this measures is the loop that ships rather than a second implementation
|
||||
//! of it; `FLY_TRACE` records it in the stream's own trace format. Without a
|
||||
//! checkpoint it refuses rather than booting the intro: a trap hunt is about a state the stream
|
||||
//! was actually in.
|
||||
|
||||
|
|
@ -46,18 +48,19 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use flybrain_core::agent::{AgentConfig, NeuralAgent, RewardEvent as NeuralReward, TickOptions};
|
||||
use flybrain_core::agent::{AgentConfig, NeuralAgent};
|
||||
use flybrain_core::dataset::load_brain_dataset_from_dir;
|
||||
use flybrain_core::decoder::gameboy::{gameboy_decoder_config_with_macros, to_button_mask};
|
||||
use flybrain_core::decoder::gameboy::gameboy_decoder_config_with_macros;
|
||||
use flybrain_core::lif::SweepPlan;
|
||||
use flybrain_gb::adapter::GameAdapter;
|
||||
use flybrain_gb::pokemon_red::PokemonRedReward;
|
||||
use flybrain_gb::ratchet::Ratchet;
|
||||
use flybrain_gb::recovery::{NeuralRecovery, recover_game};
|
||||
use flybrain_gb::{AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator};
|
||||
use flysim::config::Config;
|
||||
use flysim::frame::{Executed, FrameObserver, LegacyFrame, Parts};
|
||||
use flysim::macros::{MacroLayer, macro_layer};
|
||||
use flysim::snapshot::MacroMode;
|
||||
use flysim::trace::FrameTrace;
|
||||
|
||||
#[path = "support/ledgers.rs"]
|
||||
mod ledgers;
|
||||
|
|
@ -154,28 +157,6 @@ fn env_usize(name: &str, default: usize) -> usize {
|
|||
std::env::var(name).ok().and_then(|value| value.parse().ok()).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// The neural half of a ratchet recovery, exactly as `simloop.rs` wires it.
|
||||
struct AgentRecovery<'a> {
|
||||
agent: &'a mut NeuralAgent,
|
||||
}
|
||||
|
||||
impl NeuralRecovery for AgentRecovery<'_> {
|
||||
fn clear_decoder_holds(&mut self) {
|
||||
let ms = self.agent.network.ms;
|
||||
self.agent.decoder.clear_holds(ms);
|
||||
}
|
||||
|
||||
fn clear_eligibility(&mut self) {
|
||||
let ms = self.agent.network.ms;
|
||||
self.agent.network.plasticity.clear_eligibility(ms);
|
||||
}
|
||||
|
||||
fn set_visual_frame(&mut self, frame: &[u8]) {
|
||||
let (width, height) = (self.agent.frame.width, self.agent.frame.height);
|
||||
self.agent.network.set_visual_frame(frame, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the fly stood on one frame, and what it started on it.
|
||||
/// A refused macro and the `(map, x, y)` it was refused on.
|
||||
type RefusedAt = (&'static str, Option<(u32, u32, u32)>);
|
||||
|
|
@ -312,6 +293,161 @@ struct Trap {
|
|||
macros: usize,
|
||||
}
|
||||
|
||||
/// The hunt's look inside the stream's frame (`flysim::frame`): what it reads before and after
|
||||
/// the executor decides, and the stub readout. Everything else is the frame's own order.
|
||||
struct Hunt {
|
||||
stub: bool,
|
||||
stub_hold: usize,
|
||||
stub_next_ms: f64,
|
||||
hold_ms: f64,
|
||||
running: Option<Running>,
|
||||
scene_run: (&'static str, u64, f64),
|
||||
/// Read before `decide`, because `decide` is what starts the macro whose scene this is.
|
||||
dialog_map: Option<u8>,
|
||||
battle_sub: Option<&'static str>,
|
||||
trace: Trace,
|
||||
}
|
||||
|
||||
impl FrameObserver for Hunt {
|
||||
/// The brain is still ticked -- the frame order, the plasticity and the cost are the run's --
|
||||
/// and only the *readout* is replaced, so a stub run and a brain run differ in who chooses and
|
||||
/// in nothing else.
|
||||
fn readout(&mut self, ms: f64, bound: Option<&[String]>, active: &mut Vec<String>) {
|
||||
if !self.stub {
|
||||
return;
|
||||
}
|
||||
let hot = STUB_CHANNELS[(self.stub_hold / STUB_HOLDS_PER_CHANNEL) % STUB_CHANNELS.len()];
|
||||
if ms >= self.stub_next_ms {
|
||||
self.stub_next_ms = ms + self.hold_ms;
|
||||
self.stub_hold += 1;
|
||||
}
|
||||
*active = bound.unwrap_or_default().iter().filter(|channel| *channel == hot).cloned().collect();
|
||||
}
|
||||
|
||||
fn before_execute(&mut self, _frame: &LegacyFrame, parts: &mut Parts<'_>, _active: &[String]) {
|
||||
let layer_scene = parts.macros.as_deref().map_or("", MacroLayer::scene_name);
|
||||
self.dialog_map = (layer_scene == "dialog" || layer_scene == "unknown")
|
||||
.then(|| flybrain_gb::pokemon_red::state::player(parts.emulator).map(|p| p.map))
|
||||
.flatten();
|
||||
self.battle_sub = battle_sub_state(parts.emulator);
|
||||
if let Some(sub) = self.battle_sub {
|
||||
*self.trace.battle_frames.entry(sub).or_insert(0) += 1;
|
||||
let pad = self.trace.battle_pads.entry(sub).or_default();
|
||||
for channel in parts.macros.as_deref().map(MacroLayer::bound_channels).unwrap_or_default() {
|
||||
pad.insert(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn executed(&mut self, frame: &LegacyFrame, parts: &mut Parts<'_>, executed: &Executed) {
|
||||
let ms = parts.agent.network.ms;
|
||||
let location = frame.location;
|
||||
let trace = &mut self.trace;
|
||||
for event in &executed.events {
|
||||
match event.outcome {
|
||||
None => {
|
||||
trace.starts.push((ms, event.name));
|
||||
// Which press answered a box, and on which map: 991 `YES` in twenty brain
|
||||
// minutes is a fact about one conversation, and this is what says which.
|
||||
if let Some(map) = self.dialog_map {
|
||||
*trace.dialog_macros.entry((event.name, map)).or_insert(0) += 1;
|
||||
}
|
||||
if let Some(sub) = self.battle_sub {
|
||||
*trace.battle_starts.entry((event.name, sub)).or_insert(0) += 1;
|
||||
}
|
||||
if let Some(battle) = trace.battle_now.as_mut() {
|
||||
battle.1 += 1;
|
||||
}
|
||||
if let Some(slot) = event.name.strip_prefix("MOVE ") {
|
||||
trace.move_starts.0 += 1;
|
||||
let id = flybrain_gb::pokemon_red::state::battle(parts.emulator)
|
||||
.and_then(|battle| battle.own)
|
||||
.zip(slot.parse::<usize>().ok())
|
||||
.and_then(|(own, slot)| own.moves.get(slot - 1).copied().flatten())
|
||||
.map(|entry| entry.id);
|
||||
if id.is_some_and(|id| {
|
||||
flybrain_gb::pokemon_red::state::move_without_effect(parts.emulator, id)
|
||||
== Some(true)
|
||||
}) {
|
||||
trace.move_starts.1 += 1;
|
||||
}
|
||||
}
|
||||
self.running = Some(Running {
|
||||
name: event.name,
|
||||
from: location,
|
||||
tiles: location.into_iter().collect(),
|
||||
frames: 0,
|
||||
reach: 0,
|
||||
});
|
||||
}
|
||||
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) = self.running.take() {
|
||||
let net = match (run.from, location) {
|
||||
(Some((map, x, y)), Some((at, ax, ay))) if map == at => {
|
||||
ax.abs_diff(x) + ay.abs_diff(y)
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
trace.episodes.push(Episode {
|
||||
name: run.name,
|
||||
outcome: outcome.as_str(),
|
||||
frames: run.frames,
|
||||
tiles: run.tiles.len(),
|
||||
net,
|
||||
reach: run.reach,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let text = flybrain_gb::pokemon_red::state::text_box(parts.emulator);
|
||||
let (corners, border) = flybrain_gb::pokemon_red::state::dialog_border(parts.emulator);
|
||||
match (text.open, corners, border) {
|
||||
(true, true, true) => trace.font_corners_border += 1,
|
||||
(true, true, false) => trace.font_corners_no_border += 1,
|
||||
(true, false, _) => trace.font_no_corners += 1,
|
||||
(false, true, _) => trace.corners_no_font += 1,
|
||||
(false, false, _) => {}
|
||||
}
|
||||
}
|
||||
if let Some(layer) = parts.macros.as_deref() {
|
||||
let name = layer.scene_name();
|
||||
*trace.scenes.entry(name).or_insert(0) += 1;
|
||||
if name == self.scene_run.0 {
|
||||
self.scene_run.1 += 1;
|
||||
} else {
|
||||
self.scene_run = (name, 1, ms);
|
||||
}
|
||||
let longest = trace.longest_scene.entry(name).or_insert((0, 0.0));
|
||||
if self.scene_run.1 > longest.0 {
|
||||
*longest = (self.scene_run.1, self.scene_run.2 - trace.began_ms);
|
||||
}
|
||||
// Where the text box is, which is the half the scene histogram could not say.
|
||||
if name == "dialog" || name == "unknown" {
|
||||
let where_ = flybrain_gb::pokemon_red::state::player(parts.emulator)
|
||||
.map(|player| (player.map, player.x, player.y));
|
||||
if let Some(key) = where_ {
|
||||
*trace.dialog_frames.entry(key).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `minutes` brain minutes of the sim loop's frame order from `checkpoint`, recording where
|
||||
/// the fly stood and what it started.
|
||||
fn run(
|
||||
|
|
@ -325,8 +461,6 @@ fn run(
|
|||
) -> Trace {
|
||||
let began_wall = std::time::Instant::now();
|
||||
let stub = std::env::var("FLY_TRAP_STUB").is_ok_and(|value| value == "1");
|
||||
let mut stub_hold = 0usize;
|
||||
let mut stub_next_ms = f64::NEG_INFINITY;
|
||||
let mut emulator = Emulator::new(rom, DEFAULT_AUDIO_FREQUENCY, DEFAULT_AUDIO_FRAMES)
|
||||
.expect("binjgb should accept the cartridge");
|
||||
let mut adapter = PokemonRedReward::new();
|
||||
|
|
@ -339,34 +473,28 @@ fn run(
|
|||
.or(preset.exclusive.as_ref())
|
||||
.expect("the preset has a group")
|
||||
.hold_ms;
|
||||
let blocked_ms =
|
||||
preset.exclusive.as_ref().expect("the preset has an exclusive group").blocked_ms;
|
||||
let agent_config = AgentConfig::with_decoder(preset);
|
||||
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
||||
|
||||
emulator
|
||||
.import_state(&checkpoint.runtime.emulator)
|
||||
.expect("the checkpoint's emulator state should import");
|
||||
adapter
|
||||
.import_state(&checkpoint.runtime.reward)
|
||||
.expect("the checkpoint's reward ledger should import");
|
||||
let snapshot = (!checkpoint.runtime.ratchet_game.is_empty()).then(|| {
|
||||
flybrain_gb::ratchet::Snapshot {
|
||||
game: checkpoint.runtime.ratchet_game.clone(),
|
||||
frame: checkpoint.runtime.ratchet_frame.clone(),
|
||||
}
|
||||
});
|
||||
ratchet
|
||||
.import(Some(checkpoint.runtime.ratchet), snapshot, adapter.rank_ladder().len())
|
||||
.expect("the checkpoint's ratchet state should import");
|
||||
|
||||
let mut agent = NeuralAgent::new(Arc::clone(data), agent_config).expect("a valid agent");
|
||||
if threads > 1 {
|
||||
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
||||
}
|
||||
agent.import_state(&checkpoint.agent).expect("the checkpoint's agent should import");
|
||||
let (width, height) = (agent.frame.width, agent.frame.height);
|
||||
agent.network.set_visual_frame(&checkpoint.runtime.framebuffer, width, height);
|
||||
// The stream's own restore, into the stream's own frame: a fresh process's readout transient
|
||||
// (`restore: legacy-transient-reset`), which is what the fly has after the service restarts.
|
||||
let mut frame = LegacyFrame::new()
|
||||
.with_trace(FrameTrace::from_env().expect("FLY_TRACE should name a writable file"));
|
||||
frame
|
||||
.restore(
|
||||
&mut Parts {
|
||||
agent: &mut agent,
|
||||
emulator: &mut emulator,
|
||||
adapter: &mut adapter,
|
||||
ratchet: &mut ratchet,
|
||||
macros: None,
|
||||
},
|
||||
checkpoint,
|
||||
)
|
||||
.expect("the checkpoint should restore");
|
||||
|
||||
let mut config = Config::default();
|
||||
config.loop_.game = "pokemon-red".to_string();
|
||||
|
|
@ -391,21 +519,23 @@ fn run(
|
|||
|
||||
let began_ms = agent.network.ms;
|
||||
let until = began_ms + minutes * MINUTE_MS;
|
||||
let mut frame = emulator.framebuffer().to_vec();
|
||||
let mut payouts: Vec<flybrain_gb::RewardEvent> = Vec::new();
|
||||
let mut location = adapter.location();
|
||||
let mut blocked_since_ms = began_ms;
|
||||
let mut held_channel: Option<String> = agent.decoder.current().map(str::to_string);
|
||||
let mut rank = adapter.progress().rank;
|
||||
let mut running: Option<Running> = None;
|
||||
let rank = adapter.progress().rank;
|
||||
// A periodic one-liner for a run that is going nowhere: what the fly is standing on, what it
|
||||
// faces, and which text box the detector is looking at. Off unless asked for, because it is a
|
||||
// diagnostic and the tables above are the report.
|
||||
let trace_every_ms = env_f64("FLY_TRAP_TRACE_SECONDS", 0.0) * 1000.0;
|
||||
let mut next_trace = began_ms;
|
||||
let mut hunt = Hunt {
|
||||
stub,
|
||||
stub_hold: 0,
|
||||
stub_next_ms: f64::NEG_INFINITY,
|
||||
hold_ms,
|
||||
running: None,
|
||||
// The scene of the frames in a row, for "stuck in a text box" against "in and out of one".
|
||||
let mut scene_run: (&'static str, u64, f64) = ("", 0, began_ms);
|
||||
let mut trace = Trace {
|
||||
scene_run: ("", 0, began_ms),
|
||||
dialog_map: None,
|
||||
battle_sub: None,
|
||||
trace: Trace {
|
||||
steps: Vec::new(),
|
||||
starts: Vec::new(),
|
||||
episodes: Vec::new(),
|
||||
|
|
@ -438,192 +568,41 @@ fn run(
|
|||
refusals: BTreeMap::new(),
|
||||
refusal_run: (None, 0),
|
||||
longest_refusal_run: (0, ""),
|
||||
},
|
||||
};
|
||||
let mut rank = rank;
|
||||
|
||||
// One observation before the first frame, as the sim loop takes after a restore.
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
||||
}
|
||||
|
||||
while agent.network.ms < until {
|
||||
let rewards: Vec<NeuralReward> = payouts
|
||||
.iter()
|
||||
.map(|event| {
|
||||
NeuralReward::with_stimulation(event.value, f64::from(event.stimulation_ms))
|
||||
})
|
||||
.collect();
|
||||
let options = TickOptions { rewards: &rewards, boot: adapter.boot(), learn: true };
|
||||
let ms = agent.network.ms;
|
||||
let blocked = (blocked_ms > 0.0 && ms - blocked_since_ms >= blocked_ms)
|
||||
.then(|| agent.decoder.current().map(str::to_string))
|
||||
.flatten();
|
||||
let bound = macros.as_ref().map(MacroLayer::bound_channels);
|
||||
let result = agent
|
||||
.tick_bound(&frame, &options, blocked.as_deref(), bound.as_deref())
|
||||
.expect("a tick");
|
||||
// The brain is still ticked — the frame order, the plasticity and the cost are the run's —
|
||||
// and only the *readout* is replaced, so a stub run and a brain run differ in who chooses
|
||||
// and in nothing else.
|
||||
let active: Vec<String> = if stub {
|
||||
let hot = STUB_CHANNELS[(stub_hold / STUB_HOLDS_PER_CHANNEL) % STUB_CHANNELS.len()];
|
||||
if ms >= stub_next_ms {
|
||||
stub_next_ms = ms + hold_ms;
|
||||
stub_hold += 1;
|
||||
}
|
||||
bound
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|channel| *channel == hot)
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
result.active.clone()
|
||||
let mut parts = Parts {
|
||||
agent: &mut agent,
|
||||
emulator: &mut emulator,
|
||||
adapter: &mut adapter,
|
||||
ratchet: &mut ratchet,
|
||||
macros: macros.as_mut(),
|
||||
};
|
||||
let held = agent.decoder.current().map(str::to_string);
|
||||
if held != held_channel {
|
||||
held_channel = held;
|
||||
blocked_since_ms = ms;
|
||||
}
|
||||
|
||||
let ms = agent.network.ms;
|
||||
let mut mask = to_button_mask(&active);
|
||||
// Read before `decide`, because `decide` is what starts the macro whose scene this is.
|
||||
let layer_scene = macros.as_ref().map_or("", MacroLayer::scene_name);
|
||||
let dialog_map = (layer_scene == "dialog" || layer_scene == "unknown")
|
||||
.then(|| flybrain_gb::pokemon_red::state::player(&mut emulator).map(|p| p.map))
|
||||
.flatten();
|
||||
let battle_sub = battle_sub_state(&mut emulator);
|
||||
if let Some(sub) = battle_sub {
|
||||
*trace.battle_frames.entry(sub).or_insert(0) += 1;
|
||||
let pad = trace.battle_pads.entry(sub).or_default();
|
||||
for channel in bound.as_deref().unwrap_or_default() {
|
||||
pad.insert(channel.clone());
|
||||
}
|
||||
}
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
let decision = layer.decide(&active, mask, ms, &mut emulator, &ledger);
|
||||
mask = decision.mask;
|
||||
for event in &decision.events {
|
||||
match event.outcome {
|
||||
None => {
|
||||
trace.starts.push((ms, event.name));
|
||||
// Which press answered a box, and on which map: 991 `YES` in twenty brain
|
||||
// minutes is a fact about one conversation, and this is what says which.
|
||||
if let Some(map) = dialog_map {
|
||||
*trace.dialog_macros.entry((event.name, map)).or_insert(0) += 1;
|
||||
}
|
||||
if let Some(sub) = battle_sub {
|
||||
*trace.battle_starts.entry((event.name, sub)).or_insert(0) += 1;
|
||||
}
|
||||
if let Some(battle) = trace.battle_now.as_mut() {
|
||||
battle.1 += 1;
|
||||
}
|
||||
if let Some(slot) = event.name.strip_prefix("MOVE ") {
|
||||
trace.move_starts.0 += 1;
|
||||
let id = flybrain_gb::pokemon_red::state::battle(&mut emulator)
|
||||
.and_then(|battle| battle.own)
|
||||
.zip(slot.parse::<usize>().ok())
|
||||
.and_then(|(own, slot)| own.moves.get(slot - 1).copied().flatten())
|
||||
.map(|entry| entry.id);
|
||||
if id.is_some_and(|id| {
|
||||
flybrain_gb::pokemon_red::state::move_without_effect(&mut emulator, id)
|
||||
== Some(true)
|
||||
}) {
|
||||
trace.move_starts.1 += 1;
|
||||
}
|
||||
}
|
||||
running = Some(Running {
|
||||
name: event.name,
|
||||
from: location,
|
||||
tiles: location.into_iter().collect(),
|
||||
frames: 0,
|
||||
reach: 0,
|
||||
});
|
||||
}
|
||||
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 => {
|
||||
ax.abs_diff(x) + ay.abs_diff(y)
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
trace.episodes.push(Episode {
|
||||
name: run.name,
|
||||
outcome: outcome.as_str(),
|
||||
frames: run.frames,
|
||||
tiles: run.tiles.len(),
|
||||
net,
|
||||
reach: run.reach,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let text = flybrain_gb::pokemon_red::state::text_box(&mut emulator);
|
||||
let (corners, border) =
|
||||
flybrain_gb::pokemon_red::state::dialog_border(&mut emulator);
|
||||
match (text.open, corners, border) {
|
||||
(true, true, true) => trace.font_corners_border += 1,
|
||||
(true, true, false) => trace.font_corners_no_border += 1,
|
||||
(true, false, _) => trace.font_no_corners += 1,
|
||||
(false, true, _) => trace.corners_no_font += 1,
|
||||
(false, false, _) => {}
|
||||
}
|
||||
}
|
||||
if let Some(layer) = macros.as_ref() {
|
||||
let name = layer.scene_name();
|
||||
*trace.scenes.entry(name).or_insert(0) += 1;
|
||||
if name == scene_run.0 {
|
||||
scene_run.1 += 1;
|
||||
} else {
|
||||
scene_run = (name, 1, ms);
|
||||
}
|
||||
let longest = trace.longest_scene.entry(name).or_insert((0, 0.0));
|
||||
if scene_run.1 > longest.0 {
|
||||
*longest = (scene_run.1, scene_run.2 - began_ms);
|
||||
}
|
||||
// Where the text box is, which is the half the scene histogram could not say.
|
||||
if name == "dialog" || name == "unknown" {
|
||||
let where_ = flybrain_gb::pokemon_red::state::player(&mut emulator)
|
||||
.map(|player| (player.map, player.x, player.y));
|
||||
if let Some(key) = where_ {
|
||||
*trace.dialog_frames.entry(key).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
emulator.set_buttons(mask as u8);
|
||||
emulator.run_frame().expect("a frame should complete");
|
||||
trace.frames += 1;
|
||||
frame.copy_from_slice(emulator.framebuffer());
|
||||
|
||||
payouts = adapter.sample(&mut emulator, ms);
|
||||
for payout in &payouts {
|
||||
let entry = trace.payouts_by_kind.entry(payout.kind).or_insert((0, 0.0));
|
||||
let transition = frame.transition(&mut parts, &mut hunt).expect("a frame");
|
||||
let ms = transition.ms;
|
||||
hunt.trace.frames += 1;
|
||||
for payout in &transition.evaluated.rewards {
|
||||
let entry = hunt.trace.payouts_by_kind.entry(payout.kind).or_insert((0, 0.0));
|
||||
*entry = (entry.0 + 1, entry.1 + payout.value);
|
||||
}
|
||||
{
|
||||
use flybrain_gb::MemoryReader;
|
||||
let trace = &mut hunt.trace;
|
||||
let fighting =
|
||||
emulator.read8(flybrain_gb::pokemon_red::symbols::ram::wIsInBattle) != 0;
|
||||
let won = payouts.iter().any(|payout| matches!(payout.kind, "battle" | "trainer"));
|
||||
parts.emulator.read8(flybrain_gb::pokemon_red::symbols::ram::wIsInBattle) != 0;
|
||||
let won = transition
|
||||
.evaluated
|
||||
.rewards
|
||||
.iter()
|
||||
.any(|payout| matches!(payout.kind, "battle" | "trainer"));
|
||||
match (fighting, trace.battle_now.as_mut()) {
|
||||
(true, Some(battle)) => {
|
||||
battle.0 += 1;
|
||||
|
|
@ -638,19 +617,15 @@ fn run(
|
|||
(false, None) => {}
|
||||
}
|
||||
}
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
let ledger = AdapterLedger(&adapter);
|
||||
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
||||
}
|
||||
|
||||
if trace_every_ms > 0.0 && ms >= next_trace {
|
||||
next_trace = ms + trace_every_ms;
|
||||
let scene = macros.as_ref().map_or("", MacroLayer::scene_name);
|
||||
let scene = parts.macros.as_deref().map_or("", MacroLayer::scene_name);
|
||||
use flybrain_gb::pokemon_red::macros::cartridge::{MacroState, Tile};
|
||||
// Read before the state borrows the emulator: this is the same call the state makes,
|
||||
// and the only one that can say *which* refusal a frame is.
|
||||
let refusal = flybrain_gb::pokemon_red::state::map_grid(&mut emulator).err();
|
||||
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(&mut emulator);
|
||||
let refusal = flybrain_gb::pokemon_red::state::map_grid(parts.emulator).err();
|
||||
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(parts.emulator);
|
||||
let state: &mut dyn MacroState = &mut state;
|
||||
let player = state.player();
|
||||
let ahead = player.and_then(|player| {
|
||||
|
|
@ -658,22 +633,18 @@ fn run(
|
|||
flybrain_gb::pokemon_red::macros::path::target_at(state, ahead)
|
||||
});
|
||||
let ground = grid_line(state, player, refusal);
|
||||
let why = flybrain_gb::pokemon_red::scene::why_unknown(&mut emulator);
|
||||
let why = flybrain_gb::pokemon_red::scene::why_unknown(parts.emulator);
|
||||
println!(
|
||||
"trace {:7.2} min scene={scene:<9} player={player:?} ahead={ahead:?}\n {why}\n {ground}",
|
||||
(ms - began_ms) / MINUTE_MS
|
||||
);
|
||||
}
|
||||
|
||||
let now = adapter.location();
|
||||
if now.is_some() && now != location {
|
||||
location = now;
|
||||
blocked_since_ms = ms;
|
||||
}
|
||||
let location = frame.location;
|
||||
if let Some((map, x, y)) = location {
|
||||
trace.steps.push((ms, map, x, y));
|
||||
hunt.trace.steps.push((ms, map, x, y));
|
||||
}
|
||||
if let Some(run) = running.as_mut() {
|
||||
if let Some(run) = hunt.running.as_mut() {
|
||||
run.frames += 1;
|
||||
if let Some(at) = location {
|
||||
run.tiles.insert(at);
|
||||
|
|
@ -685,47 +656,18 @@ fn run(
|
|||
}
|
||||
}
|
||||
|
||||
let progress = adapter.progress();
|
||||
let progress = transition.evaluated.progress;
|
||||
if progress.rank != rank {
|
||||
rank = progress.rank;
|
||||
trace.rungs.push((rank, progress.rank_label, ms - began_ms));
|
||||
hunt.trace.rungs.push((rank, progress.rank_label, ms - began_ms));
|
||||
}
|
||||
let safe = adapter.safe_for_snapshot();
|
||||
let capture_due = safe && u64::from(progress.rank) > ratchet.state.best;
|
||||
let captured = capture_due.then(|| flybrain_gb::ratchet::Snapshot {
|
||||
game: emulator.export_state().expect("state export"),
|
||||
frame: frame.clone(),
|
||||
});
|
||||
let recover = ratchet.observe_with_game_over(
|
||||
safe,
|
||||
u64::from(progress.rank),
|
||||
progress.unique_locations as u64,
|
||||
ms as u64,
|
||||
adapter.game_over(),
|
||||
|| captured.expect("the ratchet only captures when a snapshot was prepared"),
|
||||
);
|
||||
if recover {
|
||||
let snapshot = flybrain_gb::ratchet::Snapshot {
|
||||
game: ratchet.game().expect("a recovery has a snapshot").to_vec(),
|
||||
frame: ratchet.frame().expect("a recovery has a framebuffer").to_vec(),
|
||||
};
|
||||
let restored = {
|
||||
let mut neural = AgentRecovery { agent: &mut agent };
|
||||
recover_game(&mut emulator, &mut adapter, &mut neural, &snapshot)
|
||||
.expect("recovering the game")
|
||||
};
|
||||
frame.copy_from_slice(&restored);
|
||||
emulator.set_buttons(0);
|
||||
trace.recoveries += 1;
|
||||
location = adapter.location();
|
||||
held_channel = None;
|
||||
blocked_since_ms = ms;
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
layer.cancel(ms);
|
||||
}
|
||||
running = None;
|
||||
let boundary = frame.boundary(&mut parts, &progress, ms).expect("the boundary");
|
||||
if boundary.rollback.is_some() {
|
||||
hunt.trace.recoveries += 1;
|
||||
hunt.running = None;
|
||||
}
|
||||
}
|
||||
let mut trace = hunt.trace;
|
||||
trace.ended_in = (
|
||||
macros.as_ref().map_or("", MacroLayer::scene_name),
|
||||
adapter.mode().to_string(),
|
||||
|
|
|
|||
|
|
@ -332,6 +332,25 @@ impl LegacyFrame {
|
|||
Ok(Transition { ticks, ms, bound, active, executed, audio, evaluated })
|
||||
}
|
||||
|
||||
/// The rest of phase B and phase C behind a stub readout, for the drivers that measure the
|
||||
/// macros without a brain (the ROM tests, the scene probe). The driver decodes its stub,
|
||||
/// calls [`LegacyFrame::execute`] with no raw mask, reads what it measures, and then this runs
|
||||
/// the frame and evaluates it. There is no commit and no ratchet.
|
||||
///
|
||||
/// A stub has no phase A to advance its clock in, so it keeps its own and advances it with the
|
||||
/// emulator frame: it decides at its clock and evaluates at `evaluate_ms`, one frame later.
|
||||
pub fn stub_advance(
|
||||
&mut self,
|
||||
macros: Option<&mut MacroLayer>,
|
||||
emulator: &mut Emulator,
|
||||
adapter: &mut dyn GameAdapter,
|
||||
evaluate_ms: f64,
|
||||
) -> Result<Evaluated> {
|
||||
self.run(emulator)?;
|
||||
let _ = self.take_frame(emulator);
|
||||
Ok(self.evaluate(emulator, adapter, macros, evaluate_ms))
|
||||
}
|
||||
|
||||
/// Phase A: brain ticks and the decode, masked to `bound`.
|
||||
pub fn prepare(
|
||||
&mut self,
|
||||
|
|
@ -427,12 +446,6 @@ impl LegacyFrame {
|
|||
Ok(self.take_frame(emulator))
|
||||
}
|
||||
|
||||
/// [`LegacyFrame::advance`] without a brain, for the stub-readout drivers.
|
||||
pub fn advance_stub(&mut self, emulator: &mut Emulator) -> Result<Vec<u8>> {
|
||||
self.run(emulator)?;
|
||||
Ok(self.take_frame(emulator))
|
||||
}
|
||||
|
||||
fn run(&mut self, emulator: &mut Emulator) -> Result<()> {
|
||||
emulator.set_buttons(self.buttons as u8);
|
||||
emulator
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ use flybrain_gb::{
|
|||
AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, GameAdapter,
|
||||
};
|
||||
use flysim::config::Config;
|
||||
use flysim::frame::LegacyFrame;
|
||||
use flysim::macros::{MacroLayer, macro_layer};
|
||||
use flysim::snapshot::MacroMode;
|
||||
|
||||
|
|
@ -92,6 +93,8 @@ struct Run {
|
|||
gb: Emulator,
|
||||
adapter: PokemonRedReward,
|
||||
layer: MacroLayer,
|
||||
/// The stream's frame (`flysim::frame::LegacyFrame`), behind the stub readout.
|
||||
legacy: LegacyFrame,
|
||||
decoder: PopulationDecoder,
|
||||
channels: Vec<&'static str>,
|
||||
ms: f64,
|
||||
|
|
@ -124,6 +127,7 @@ impl Run {
|
|||
gb,
|
||||
adapter,
|
||||
layer,
|
||||
legacy: LegacyFrame::new(),
|
||||
decoder,
|
||||
channels,
|
||||
ms: 0.0,
|
||||
|
|
@ -161,18 +165,14 @@ impl Run {
|
|||
};
|
||||
let bound = self.layer.bound_channels();
|
||||
let active = self.decoder.decode_bound(&rates(hot), self.ms, false, None, Some(&bound));
|
||||
let mask = {
|
||||
let ledger = AdapterLedger(&self.adapter);
|
||||
self.layer.decide(&active, 0, self.ms, &mut self.gb, &ledger).mask
|
||||
};
|
||||
self.gb.set_buttons(mask as u8);
|
||||
self.gb.run_frame().expect("a frame should complete");
|
||||
self.legacy.execute(Some(&mut self.layer), &active, 0, self.ms, &mut self.gb, &self.adapter);
|
||||
self.ms += MS_PER_FRAME;
|
||||
self.frame += 1;
|
||||
let ms = self.ms;
|
||||
self.payouts.extend(self.adapter.sample(&mut self.gb, ms));
|
||||
let ledger = AdapterLedger(&self.adapter);
|
||||
let _ = self.layer.observe(&mut self.gb, &ledger, ms);
|
||||
let evaluated = self
|
||||
.legacy
|
||||
.stub_advance(Some(&mut self.layer), &mut self.gb, &mut self.adapter, self.ms)
|
||||
.expect("a frame should complete");
|
||||
self.payouts.extend(evaluated.rewards);
|
||||
}
|
||||
|
||||
fn catches(&self) -> Vec<&RewardEvent> {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ use flybrain_gb::{
|
|||
AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, GameAdapter, buttons,
|
||||
};
|
||||
use flysim::config::Config;
|
||||
use flysim::frame::LegacyFrame;
|
||||
use flysim::macros::{MacroLayer, macro_layer};
|
||||
use flysim::snapshot::MacroMode;
|
||||
|
||||
|
|
@ -147,6 +148,8 @@ struct Run {
|
|||
gb: Emulator,
|
||||
adapter: PokemonRedReward,
|
||||
layer: MacroLayer,
|
||||
/// The stream's frame (`flysim::frame::LegacyFrame`), behind the stub readout.
|
||||
frame: LegacyFrame,
|
||||
/// The readout under test: the shipping decoder, fed by hand.
|
||||
decoder: PopulationDecoder,
|
||||
/// The macro channels, in the decoder's own order, for the rotation.
|
||||
|
|
@ -402,6 +405,7 @@ impl Run {
|
|||
gb,
|
||||
adapter,
|
||||
layer,
|
||||
frame: LegacyFrame::new(),
|
||||
decoder,
|
||||
channels,
|
||||
ms,
|
||||
|
|
@ -520,6 +524,7 @@ impl Run {
|
|||
gb,
|
||||
adapter,
|
||||
layer,
|
||||
frame: LegacyFrame::new(),
|
||||
decoder,
|
||||
channels,
|
||||
ms,
|
||||
|
|
@ -815,9 +820,15 @@ impl Run {
|
|||
self.talk_on_pad = talk_bound;
|
||||
let active =
|
||||
self.decoder.decode_bound(&rates(hot), self.ms, false, None, Some(&bound));
|
||||
let (mask, started, blocked, done) = {
|
||||
let ledger = AdapterLedger(&self.adapter);
|
||||
let decision = self.layer.decide(&active, 0, self.ms, &mut self.gb, &ledger);
|
||||
let (started, blocked, done) = {
|
||||
let decision = self.frame.execute(
|
||||
Some(&mut self.layer),
|
||||
&active,
|
||||
0,
|
||||
self.ms,
|
||||
&mut self.gb,
|
||||
&self.adapter,
|
||||
);
|
||||
let started: Vec<&'static str> = decision
|
||||
.events
|
||||
.iter()
|
||||
|
|
@ -840,7 +851,7 @@ impl Run {
|
|||
})
|
||||
.map(|event| event.name)
|
||||
.collect();
|
||||
(decision.mask, started, blocked, done)
|
||||
(started, blocked, done)
|
||||
};
|
||||
// Row 54's own measure, taken before the starts below so that a macro that finishes and
|
||||
// another that starts on the same frame are not confused for one another.
|
||||
|
|
@ -963,15 +974,10 @@ impl Run {
|
|||
let (x, y) = self.tile();
|
||||
self.started_at = Some((self.map(), x, y));
|
||||
}
|
||||
self.gb.set_buttons(mask as u8);
|
||||
self.gb.run_frame().expect("a frame should complete");
|
||||
self.ms += MS_PER_FRAME;
|
||||
let ms = self.ms;
|
||||
self.adapter.sample(&mut self.gb, ms);
|
||||
{
|
||||
let ledger = AdapterLedger(&self.adapter);
|
||||
let _ = self.layer.observe(&mut self.gb, &ledger, ms);
|
||||
}
|
||||
self.frame
|
||||
.stub_advance(Some(&mut self.layer), &mut self.gb, &mut self.adapter, self.ms)
|
||||
.expect("a frame should complete");
|
||||
// Battle boundaries, after the frame: what a battle cost in macros, and whether it ended.
|
||||
let now_in_battle = self.in_battle() != 0;
|
||||
match (self.was_in_battle, now_in_battle) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue