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
|
/// 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.
|
/// -- 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
|
/// `flysim` does not come through here: its frame (`flysim::frame::LegacyFrame`) drives the
|
||||||
/// itself, so that the macro layer can read the emulator between the two), but the bench that
|
/// network and the decoder itself, so that the macro layer can read the emulator between the
|
||||||
/// measures the two arms against each other does, and a bench whose macro group could win a
|
/// two, and it installs a frame and its rewards straight after the frame rather than after the
|
||||||
/// channel the scene never bound would be measuring something the stream cannot do.
|
/// next ticks. Every `flysim` harness runs that frame too.
|
||||||
pub fn tick_bound(
|
pub fn tick_bound(
|
||||||
&mut self,
|
&mut self,
|
||||||
frame: &[u8],
|
frame: &[u8],
|
||||||
|
|
|
||||||
|
|
@ -70,19 +70,20 @@ use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use flybrain_core::agent::{
|
use flybrain_core::agent::{
|
||||||
AgentConfig, NeuralAgent, RewardEvent as NeuralReward, TickOptions,
|
AgentConfig, NeuralAgent,
|
||||||
};
|
};
|
||||||
use flybrain_core::dataset::load_brain_dataset_from_dir;
|
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_core::lif::SweepPlan;
|
||||||
use flybrain_gb::adapter::GameAdapter;
|
use flybrain_gb::adapter::GameAdapter;
|
||||||
use flybrain_gb::pokemon_red::PokemonRedReward;
|
use flybrain_gb::pokemon_red::PokemonRedReward;
|
||||||
use flybrain_gb::ratchet::Ratchet;
|
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 flybrain_gb::{AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, buttons};
|
||||||
use flysim::config::Config;
|
use flysim::config::Config;
|
||||||
|
use flysim::frame::{Executed, FrameObserver, LegacyFrame, Parts};
|
||||||
use flysim::macros::{MacroLayer, OutcomeCounts, Silence, macro_layer};
|
use flysim::macros::{MacroLayer, OutcomeCounts, Silence, macro_layer};
|
||||||
use flysim::snapshot::MacroMode;
|
use flysim::snapshot::MacroMode;
|
||||||
|
use flysim::trace::FrameTrace;
|
||||||
|
|
||||||
/// `constants/map_constants.asm`: Red's bedroom, where a cold boot ends up.
|
/// `constants/map_constants.asm`: Red's bedroom, where a cold boot ends up.
|
||||||
const REDS_HOUSE_2F: u32 = 0x26;
|
const REDS_HOUSE_2F: u32 = 0x26;
|
||||||
|
|
@ -103,25 +104,12 @@ fn emulator(rom: &[u8]) -> Emulator {
|
||||||
.expect("binjgb should accept the cartridge")
|
.expect("binjgb should accept the cartridge")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The neural half of a ratchet recovery, exactly as `simloop.rs` wires it.
|
/// Whether a macro owned the buttons of a frame: read as the executor hands the mask over.
|
||||||
struct AgentRecovery<'a> {
|
struct MacroOwned(bool);
|
||||||
agent: &'a mut NeuralAgent,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NeuralRecovery for AgentRecovery<'_> {
|
impl FrameObserver for MacroOwned {
|
||||||
fn clear_decoder_holds(&mut self) {
|
fn executed(&mut self, _frame: &LegacyFrame, parts: &mut Parts<'_>, _executed: &Executed) {
|
||||||
let ms = self.agent.network.ms;
|
self.0 = parts.macros.as_deref().is_some_and(|layer| layer.running().is_some());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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.
|
/// 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 frame is `flysim::frame::LegacyFrame`, the one the stream runs, so the arm measures the
|
||||||
/// the payouts handed to a tick are the ones the previous tick's buttons produced. The macro layer
|
/// wiring under test rather than a second implementation of it. (Before FND-01 the arms ticked the
|
||||||
/// is consulted at exactly the two points the loop consults it — after the decode, before the
|
/// brain through `NeuralAgent::tick`, one frame behind the stream's order.)
|
||||||
/// 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.
|
|
||||||
fn run_arm(
|
fn run_arm(
|
||||||
rom: &[u8],
|
rom: &[u8],
|
||||||
data: &Arc<flybrain_core::dataset::BrainDataset>,
|
data: &Arc<flybrain_core::dataset::BrainDataset>,
|
||||||
|
|
@ -270,48 +256,39 @@ fn run_arm(
|
||||||
.or(preset.exclusive.as_ref())
|
.or(preset.exclusive.as_ref())
|
||||||
.expect("the preset has a group")
|
.expect("the preset has a group")
|
||||||
.hold_ms;
|
.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 agent_config = AgentConfig::with_decoder(preset);
|
||||||
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
||||||
let mut arm = Arm { mode: mode.as_str(), ..Arm::default() };
|
let mut arm = Arm { mode: mode.as_str(), ..Arm::default() };
|
||||||
|
|
||||||
match start {
|
if let Start::Fresh { warmup_ms, .. } = start {
|
||||||
Start::Fresh { state, warmup_ms } => {
|
agent_config.warmup_ms = *warmup_ms;
|
||||||
emulator.import_state(state).expect("the booted state should import");
|
|
||||||
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");
|
let mut agent = NeuralAgent::new(Arc::clone(data), agent_config).expect("a valid agent");
|
||||||
if threads > 1 {
|
if threads > 1 {
|
||||||
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
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 {
|
match start {
|
||||||
Start::Fresh { .. } => {
|
Start::Fresh { state, warmup_ms: _ } => {
|
||||||
agent.warmup(Some(emulator.framebuffer())).expect("warm-up");
|
emulator.import_state(state).expect("the booted state should import");
|
||||||
}
|
frame.frame_buffer.copy_from_slice(emulator.framebuffer());
|
||||||
Start::Live { checkpoint } => {
|
agent.warmup(Some(&frame.frame_buffer)).expect("warm-up");
|
||||||
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.
|
||||||
|
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
|
// 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 began_ms = agent.network.ms;
|
||||||
let until = began_ms + hours * HOUR_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 mut rank = adapter.progress().rank;
|
||||||
let tiles_at_start = adapter.progress().unique_locations;
|
let tiles_at_start = adapter.progress().unique_locations;
|
||||||
arm.rungs.push((rank, adapter.progress().rank_label, 0.0));
|
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,
|
// One observation before the first frame, as the sim loop takes after a restore, so frame one
|
||||||
// which presses nothing. That is one frame, and it is the honest starting state.
|
// is decided on a real palette.
|
||||||
if let Some(layer) = macros.as_mut() {
|
if let Some(layer) = macros.as_mut() {
|
||||||
let ledger = AdapterLedger(&adapter);
|
let ledger = AdapterLedger(&adapter);
|
||||||
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
while agent.network.ms < until {
|
while agent.network.ms < until {
|
||||||
let rewards: Vec<NeuralReward> = payouts
|
let mut parts = Parts {
|
||||||
.iter()
|
agent: &mut agent,
|
||||||
.map(|event| {
|
emulator: &mut emulator,
|
||||||
NeuralReward::with_stimulation(event.value, f64::from(event.stimulation_ms))
|
adapter: &mut adapter,
|
||||||
})
|
ratchet: &mut ratchet,
|
||||||
.collect();
|
macros: macros.as_mut(),
|
||||||
let options = TickOptions { rewards: &rewards, boot: adapter.boot(), learn: true };
|
};
|
||||||
|
let mut owned = MacroOwned(false);
|
||||||
// The blocked-direction cooldown's input, as `simloop.rs` computes it.
|
let transition = frame.transition(&mut parts, &mut owned).expect("a frame");
|
||||||
let ms = agent.network.ms;
|
let ms = transition.ms;
|
||||||
let blocked = (blocked_ms > 0.0 && ms - blocked_since_ms >= blocked_ms)
|
let executed = &transition.executed;
|
||||||
.then(|| agent.decoder.current().map(str::to_string))
|
if let Some(layer) = parts.macros.as_deref() {
|
||||||
.flatten();
|
if let Some(silence) = executed.silence {
|
||||||
// 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 {
|
|
||||||
*arm.silence.entry(silence.label()).or_insert(0) += 1;
|
*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;
|
*arm.by_rank.entry(event.slot).or_insert(0) += 1;
|
||||||
}
|
}
|
||||||
if layer.running().is_some() {
|
if owned.0 {
|
||||||
arm.macro_frames += 1;
|
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;
|
arm.idle_frames += 1;
|
||||||
}
|
}
|
||||||
emulator.set_buttons(mask as u8);
|
|
||||||
emulator.run_frame().expect("a frame should complete");
|
|
||||||
arm.frames += 1;
|
arm.frames += 1;
|
||||||
frame.copy_from_slice(emulator.framebuffer());
|
for event in &transition.evaluated.rewards {
|
||||||
|
|
||||||
// Steps 7 to 9, then the scene.
|
|
||||||
payouts = adapter.sample(&mut emulator, ms);
|
|
||||||
for event in &payouts {
|
|
||||||
arm.reward += event.value;
|
arm.reward += event.value;
|
||||||
*arm.payouts.entry(event.kind).or_insert(0) += 1;
|
*arm.payouts.entry(event.kind).or_insert(0) += 1;
|
||||||
}
|
}
|
||||||
if let Some(layer) = macros.as_mut() {
|
arm.digest = hash(arm.digest, u64::from(executed.mask));
|
||||||
let ledger = AdapterLedger(&adapter);
|
if let Some((map, x, y)) = frame.location {
|
||||||
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(map) << 32 | u64::from(x) << 16 | u64::from(y));
|
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 = transition.evaluated.progress;
|
||||||
let progress = adapter.progress();
|
|
||||||
if progress.rank != rank {
|
if progress.rank != rank {
|
||||||
rank = progress.rank;
|
rank = progress.rank;
|
||||||
arm.rungs.push((rank, progress.rank_label, ms - began_ms));
|
arm.rungs.push((rank, progress.rank_label, ms - began_ms));
|
||||||
}
|
}
|
||||||
let safe = adapter.safe_for_snapshot();
|
let boundary = frame.boundary(&mut parts, &progress, ms).expect("the boundary");
|
||||||
let capture_due = safe && u64::from(progress.rank) > ratchet.state.best;
|
if boundary.rollback.is_some() {
|
||||||
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);
|
|
||||||
arm.recoveries += 1;
|
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 std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
use flybrain_core::agent::{
|
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::dataset::load_brain_dataset_from_dir;
|
||||||
use flybrain_core::decoder::gameboy::{GAMEBOY_BUTTONS, gameboy_decoder_config, to_button_mask};
|
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::symbols::ram;
|
||||||
use flybrain_gb::pokemon_red::{PokemonRedReward, SUPPORTED_ROM};
|
use flybrain_gb::pokemon_red::{PokemonRedReward, SUPPORTED_ROM};
|
||||||
use flybrain_gb::ratchet::Ratchet;
|
use flybrain_gb::ratchet::Ratchet;
|
||||||
use flybrain_gb::recovery::{NeuralRecovery, recover_game};
|
|
||||||
use flybrain_gb::{DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, buttons};
|
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.
|
/// `constants/map_constants.asm`: Red's bedroom and the ground floor of his house.
|
||||||
const REDS_HOUSE_2F: u32 = 0x26;
|
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 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.
|
/// Where one brain run starts.
|
||||||
enum Start<'a> {
|
enum Start<'a> {
|
||||||
/// A save state and the map it stands in: a fresh fly, warmed up here.
|
/// 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)
|
(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
|
/// 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.
|
/// live checkpoint -- the real reward ledger and the real ratchet.
|
||||||
///
|
///
|
||||||
|
|
@ -697,174 +766,81 @@ fn brain_trace(
|
||||||
let mut emulator = emulator(rom);
|
let mut emulator = emulator(rom);
|
||||||
let mut adapter = PokemonRedReward::new();
|
let mut adapter = PokemonRedReward::new();
|
||||||
let mut agent_config = AgentConfig::with_decoder(config.clone());
|
let mut agent_config = AgentConfig::with_decoder(config.clone());
|
||||||
let (hold_ms, blocked_ms) = {
|
let hold_ms =
|
||||||
let group = config.exclusive.as_ref().expect("the Game Boy preset has an exclusive group");
|
config.exclusive.as_ref().expect("the Game Boy preset has an exclusive group").hold_ms;
|
||||||
(group.hold_ms, group.blocked_ms)
|
|
||||||
};
|
|
||||||
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
||||||
let mut trace = Trace::default();
|
let mut trace = Trace::default();
|
||||||
|
if let Start::Fresh { warmup_ms, .. } = &start {
|
||||||
let start_map = match &start {
|
agent_config.warmup_ms = *warmup_ms;
|
||||||
Start::Fresh { state, map, warmup_ms } => {
|
}
|
||||||
emulator.import_state(state).expect("the starting state should import");
|
|
||||||
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");
|
let mut agent = NeuralAgent::new(Arc::clone(data), agent_config).expect("a valid agent");
|
||||||
if threads > 1 {
|
if threads > 1 {
|
||||||
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
||||||
}
|
}
|
||||||
match &start {
|
// The stream's own frame (`flysim::frame::LegacyFrame`), in raw mode: no macro layer.
|
||||||
Start::Fresh { .. } => agent.warmup(Some(emulator.framebuffer())).expect("warm-up"),
|
let mut frame = LegacyFrame::new();
|
||||||
Start::Live { checkpoint, rng } => {
|
let start_map = match &start {
|
||||||
let mut state = checkpoint.agent.clone();
|
Start::Fresh { state, map, .. } => {
|
||||||
state.network.rng = *rng;
|
emulator.import_state(state).expect("the starting state should import");
|
||||||
agent.import_state(&state).expect("the checkpoint's agent state should import");
|
frame.frame_buffer.copy_from_slice(emulator.framebuffer());
|
||||||
let (width, height) = (agent.frame.width, agent.frame.height);
|
agent.warmup(Some(&frame.frame_buffer)).expect("warm-up");
|
||||||
agent.network.set_visual_frame(&checkpoint.runtime.framebuffer, width, height);
|
*map
|
||||||
}
|
}
|
||||||
}
|
Start::Live { checkpoint, rng } => {
|
||||||
|
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);
|
trace.maps.push(start_map);
|
||||||
let began_ms = agent.network.ms;
|
let began_ms = agent.network.ms;
|
||||||
let until = began_ms + minutes * 60_000.0;
|
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:
|
let mut escape = Escape {
|
||||||
// the frame and the payouts handed to a tick are the ones the previous tick's buttons produced.
|
before: None,
|
||||||
let mut frame = emulator.framebuffer().to_vec();
|
hold_start: (frame.location, began_ms),
|
||||||
let mut payouts: Vec<flybrain_gb::RewardEvent> = Vec::new();
|
run_winner: None,
|
||||||
let mut location = adapter.location();
|
run_length: 0.0,
|
||||||
// The blocked-direction cooldown's window, restarted by a move *or* by a new winner, exactly
|
start_map,
|
||||||
// as `simloop.rs` restarts it: a direction that has just won has not had a hold to move in yet.
|
exits,
|
||||||
let mut blocked_since_ms = began_ms;
|
hold_ms,
|
||||||
let mut held_channel: Option<String> = agent.decoder.current().map(str::to_string);
|
trace,
|
||||||
let mut hold_start = (location, began_ms);
|
};
|
||||||
let mut run_winner: Option<String> = None;
|
|
||||||
let mut run_length = 0.0f64;
|
|
||||||
// Reported on its own: the longest stretch with no movement at all, whatever was held.
|
// Reported on its own: the longest stretch with no movement at all, whatever was held.
|
||||||
let mut still_since_ms = began_ms;
|
let mut still_since_ms = began_ms;
|
||||||
let tiles_at_start = adapter.progress().unique_locations;
|
let tiles_at_start = adapter.progress().unique_locations;
|
||||||
trace.tiles = tiles_at_start;
|
escape.trace.tiles = tiles_at_start;
|
||||||
|
|
||||||
while agent.network.ms < until {
|
while agent.network.ms < until {
|
||||||
let rewards: Vec<NeuralReward> = payouts
|
let mut parts = Parts {
|
||||||
.iter()
|
agent: &mut agent,
|
||||||
.map(|event| {
|
emulator: &mut emulator,
|
||||||
NeuralReward::with_stimulation(event.value, f64::from(event.stimulation_ms))
|
adapter: &mut adapter,
|
||||||
})
|
ratchet: &mut ratchet,
|
||||||
.collect();
|
macros: None,
|
||||||
let options = TickOptions { rewards: &rewards, boot: adapter.boot(), learn: true };
|
};
|
||||||
|
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;
|
||||||
|
|
||||||
// The blocked-direction cooldown's input, computed the way `simloop.rs` computes it: the
|
let location = frame.location;
|
||||||
// channel the readout is holding, once the adapter's location has stood still for a whole
|
if location != location_before {
|
||||||
// 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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
still_since_ms = ms;
|
still_since_ms = ms;
|
||||||
}
|
}
|
||||||
trace.longest_still_ms = trace.longest_still_ms.max(ms - still_since_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;
|
*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.
|
// The ratchet, with the adapter's own policy and the checkpoint's own budget.
|
||||||
let progress = adapter.progress();
|
let progress = transition.evaluated.progress;
|
||||||
let safe = adapter.safe_for_snapshot();
|
let boundary = frame.boundary(&mut parts, &progress, ms).expect("the boundary");
|
||||||
let capture_due = safe && u64::from(progress.rank) > ratchet.state.best;
|
if boundary.rollback.is_some() {
|
||||||
let captured = capture_due.then(|| flybrain_gb::ratchet::Snapshot {
|
escape.trace.recoveries += 1;
|
||||||
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;
|
|
||||||
still_since_ms = ms;
|
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 };
|
let Some(map) = adapter.map_id() else { continue };
|
||||||
if trace.maps.last() != Some(&map) {
|
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 {
|
if run_length > 0.0 {
|
||||||
trace.runs.push(run_length);
|
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
|
// [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.
|
// bytes say "the move list" -- which is the whole of what the seam read before row 50.
|
||||||
let mut readings = [[0usize; 2]; 2];
|
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!("\n## Row 50: every battle frame, pressed at\n");
|
||||||
println!("```");
|
println!("```");
|
||||||
|
|
@ -823,18 +825,9 @@ fn accept_survey(
|
||||||
}
|
}
|
||||||
let bound = layer.bound_channels();
|
let bound = layer.bound_channels();
|
||||||
let active = decoder.decode_bound(&rates(hot), *ms, false, None, Some(&bound));
|
let active = decoder.decode_bound(&rates(hot), *ms, false, None, Some(&bound));
|
||||||
let mask = {
|
legacy.execute(Some(&mut *layer), &active, 0, *ms, gb, &*adapter);
|
||||||
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");
|
|
||||||
*ms += MS_PER_FRAME;
|
*ms += MS_PER_FRAME;
|
||||||
adapter.sample(gb, *ms);
|
legacy.stub_advance(Some(&mut *layer), gb, adapter, *ms).expect("a frame should complete");
|
||||||
{
|
|
||||||
let ledger = AdapterLedger(adapter);
|
|
||||||
let _ = layer.observe(gb, &ledger, *ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((name, own_turn, forced)) = battle_reading(gb, adapter) else { continue };
|
let Some((name, own_turn, forced)) = battle_reading(gb, adapter) else { continue };
|
||||||
let geom = move_cursor_geometry(gb);
|
let geom = move_cursor_geometry(gb);
|
||||||
|
|
@ -1801,6 +1794,8 @@ fn main() {
|
||||||
let mut noattack = 0usize;
|
let mut noattack = 0usize;
|
||||||
let mut before = (0u8, 0u8, 0u8);
|
let mut before = (0u8, 0u8, 0u8);
|
||||||
let mut surveyed = 0usize;
|
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 {
|
for frame in 0..budget {
|
||||||
let bursting = ms < next_burst + BURST_MS;
|
let bursting = ms < next_burst + BURST_MS;
|
||||||
let hot = bursting.then(|| channels[(burst / HOLDS_PER_SLOT) % channels.len()]);
|
let hot = bursting.then(|| channels[(burst / HOLDS_PER_SLOT) % channels.len()]);
|
||||||
|
|
@ -1810,18 +1805,11 @@ fn main() {
|
||||||
}
|
}
|
||||||
let bound = layer.bound_channels();
|
let bound = layer.bound_channels();
|
||||||
let active = decoder.decode_bound(&rates(hot), ms, false, None, Some(&bound));
|
let active = decoder.decode_bound(&rates(hot), ms, false, None, Some(&bound));
|
||||||
let mask = {
|
legacy.execute(Some(&mut layer), &active, 0, ms, &mut gb, &adapter);
|
||||||
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");
|
|
||||||
ms += MS_PER_FRAME;
|
ms += MS_PER_FRAME;
|
||||||
adapter.sample(&mut gb, ms);
|
legacy
|
||||||
{
|
.stub_advance(Some(&mut layer), &mut gb, &mut adapter, ms)
|
||||||
let ledger = AdapterLedger(&adapter);
|
.expect("a frame should complete");
|
||||||
let _ = layer.observe(&mut gb, &ledger, ms);
|
|
||||||
}
|
|
||||||
if catch_script
|
if catch_script
|
||||||
&& gb.read8(ram::wSimulatedJoypadStatesIndex) != 0
|
&& gb.read8(ram::wSimulatedJoypadStatesIndex) != 0
|
||||||
&& gb.read8(ram::wCurMap) == 1
|
&& gb.read8(ram::wCurMap) == 1
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,11 @@
|
||||||
//! | `FLY_TRAP_THREADS` | 4 | sweep threads |
|
//! | `FLY_TRAP_THREADS` | 4 | sweep threads |
|
||||||
//! | `FLY_TRAP_SEED` | 20260917 | seeds the palette |
|
//! | `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_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
|
//! The frame is `flysim::frame::LegacyFrame`, the one the stream runs, restored the way the stream
|
||||||
//! measures is the loop that ships rather than a second implementation of it. Without a
|
//! 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
|
//! checkpoint it refuses rather than booting the intro: a trap hunt is about a state the stream
|
||||||
//! was actually in.
|
//! was actually in.
|
||||||
|
|
||||||
|
|
@ -46,18 +48,19 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
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::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_core::lif::SweepPlan;
|
||||||
use flybrain_gb::adapter::GameAdapter;
|
use flybrain_gb::adapter::GameAdapter;
|
||||||
use flybrain_gb::pokemon_red::PokemonRedReward;
|
use flybrain_gb::pokemon_red::PokemonRedReward;
|
||||||
use flybrain_gb::ratchet::Ratchet;
|
use flybrain_gb::ratchet::Ratchet;
|
||||||
use flybrain_gb::recovery::{NeuralRecovery, recover_game};
|
|
||||||
use flybrain_gb::{AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator};
|
use flybrain_gb::{AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator};
|
||||||
use flysim::config::Config;
|
use flysim::config::Config;
|
||||||
|
use flysim::frame::{Executed, FrameObserver, LegacyFrame, Parts};
|
||||||
use flysim::macros::{MacroLayer, macro_layer};
|
use flysim::macros::{MacroLayer, macro_layer};
|
||||||
use flysim::snapshot::MacroMode;
|
use flysim::snapshot::MacroMode;
|
||||||
|
use flysim::trace::FrameTrace;
|
||||||
|
|
||||||
#[path = "support/ledgers.rs"]
|
#[path = "support/ledgers.rs"]
|
||||||
mod ledgers;
|
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)
|
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.
|
/// 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.
|
/// A refused macro and the `(map, x, y)` it was refused on.
|
||||||
type RefusedAt = (&'static str, Option<(u32, u32, u32)>);
|
type RefusedAt = (&'static str, Option<(u32, u32, u32)>);
|
||||||
|
|
@ -312,6 +293,161 @@ struct Trap {
|
||||||
macros: usize,
|
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
|
/// Run `minutes` brain minutes of the sim loop's frame order from `checkpoint`, recording where
|
||||||
/// the fly stood and what it started.
|
/// the fly stood and what it started.
|
||||||
fn run(
|
fn run(
|
||||||
|
|
@ -325,8 +461,6 @@ fn run(
|
||||||
) -> Trace {
|
) -> Trace {
|
||||||
let began_wall = std::time::Instant::now();
|
let began_wall = std::time::Instant::now();
|
||||||
let stub = std::env::var("FLY_TRAP_STUB").is_ok_and(|value| value == "1");
|
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)
|
let mut emulator = Emulator::new(rom, DEFAULT_AUDIO_FREQUENCY, DEFAULT_AUDIO_FRAMES)
|
||||||
.expect("binjgb should accept the cartridge");
|
.expect("binjgb should accept the cartridge");
|
||||||
let mut adapter = PokemonRedReward::new();
|
let mut adapter = PokemonRedReward::new();
|
||||||
|
|
@ -339,34 +473,28 @@ fn run(
|
||||||
.or(preset.exclusive.as_ref())
|
.or(preset.exclusive.as_ref())
|
||||||
.expect("the preset has a group")
|
.expect("the preset has a group")
|
||||||
.hold_ms;
|
.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 agent_config = AgentConfig::with_decoder(preset);
|
||||||
let mut ratchet = Ratchet::with_policy(adapter.recovery_policy());
|
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");
|
let mut agent = NeuralAgent::new(Arc::clone(data), agent_config).expect("a valid agent");
|
||||||
if threads > 1 {
|
if threads > 1 {
|
||||||
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
agent.set_sweep_plan(SweepPlan::with_threads(threads).expect("a sweep plan"));
|
||||||
}
|
}
|
||||||
agent.import_state(&checkpoint.agent).expect("the checkpoint's agent should import");
|
// The stream's own restore, into the stream's own frame: a fresh process's readout transient
|
||||||
let (width, height) = (agent.frame.width, agent.frame.height);
|
// (`restore: legacy-transient-reset`), which is what the fly has after the service restarts.
|
||||||
agent.network.set_visual_frame(&checkpoint.runtime.framebuffer, width, height);
|
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();
|
let mut config = Config::default();
|
||||||
config.loop_.game = "pokemon-red".to_string();
|
config.loop_.game = "pokemon-red".to_string();
|
||||||
|
|
@ -391,239 +519,90 @@ fn run(
|
||||||
|
|
||||||
let began_ms = agent.network.ms;
|
let began_ms = agent.network.ms;
|
||||||
let until = began_ms + minutes * MINUTE_MS;
|
let until = began_ms + minutes * MINUTE_MS;
|
||||||
let mut frame = emulator.framebuffer().to_vec();
|
let rank = adapter.progress().rank;
|
||||||
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;
|
|
||||||
// A periodic one-liner for a run that is going nowhere: what the fly is standing on, what it
|
// 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
|
// 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.
|
// diagnostic and the tables above are the report.
|
||||||
let trace_every_ms = env_f64("FLY_TRAP_TRACE_SECONDS", 0.0) * 1000.0;
|
let trace_every_ms = env_f64("FLY_TRAP_TRACE_SECONDS", 0.0) * 1000.0;
|
||||||
let mut next_trace = began_ms;
|
let mut next_trace = began_ms;
|
||||||
// The scene of the frames in a row, for "stuck in a text box" against "in and out of one".
|
let mut hunt = Hunt {
|
||||||
let mut scene_run: (&'static str, u64, f64) = ("", 0, began_ms);
|
stub,
|
||||||
let mut trace = Trace {
|
stub_hold: 0,
|
||||||
steps: Vec::new(),
|
stub_next_ms: f64::NEG_INFINITY,
|
||||||
starts: Vec::new(),
|
hold_ms,
|
||||||
episodes: Vec::new(),
|
running: None,
|
||||||
began_ms,
|
// The scene of the frames in a row, for "stuck in a text box" against "in and out of one".
|
||||||
ended_ms: began_ms,
|
scene_run: ("", 0, began_ms),
|
||||||
frames: 0,
|
dialog_map: None,
|
||||||
recoveries: 0,
|
battle_sub: None,
|
||||||
rungs: vec![(rank, adapter.progress().rank_label, 0.0)],
|
trace: Trace {
|
||||||
outcomes: BTreeMap::new(),
|
steps: Vec::new(),
|
||||||
scenes: BTreeMap::new(),
|
starts: Vec::new(),
|
||||||
dialog_frames: BTreeMap::new(),
|
episodes: Vec::new(),
|
||||||
dialog_macros: BTreeMap::new(),
|
began_ms,
|
||||||
ended_in: ("", String::new()),
|
ended_ms: began_ms,
|
||||||
longest_scene: BTreeMap::new(),
|
frames: 0,
|
||||||
ended_why: String::new(),
|
recoveries: 0,
|
||||||
ended_grid: String::new(),
|
rungs: vec![(rank, adapter.progress().rank_label, 0.0)],
|
||||||
font_corners_border: 0,
|
outcomes: BTreeMap::new(),
|
||||||
font_corners_no_border: 0,
|
scenes: BTreeMap::new(),
|
||||||
font_no_corners: 0,
|
dialog_frames: BTreeMap::new(),
|
||||||
corners_no_font: 0,
|
dialog_macros: BTreeMap::new(),
|
||||||
battle_frames: BTreeMap::new(),
|
ended_in: ("", String::new()),
|
||||||
battle_starts: BTreeMap::new(),
|
longest_scene: BTreeMap::new(),
|
||||||
battle_pads: BTreeMap::new(),
|
ended_why: String::new(),
|
||||||
battles: Vec::new(),
|
ended_grid: String::new(),
|
||||||
battle_now: None,
|
font_corners_border: 0,
|
||||||
payouts_by_kind: BTreeMap::new(),
|
font_corners_no_border: 0,
|
||||||
move_starts: (0, 0),
|
font_no_corners: 0,
|
||||||
wall_seconds: 0.0,
|
corners_no_font: 0,
|
||||||
seeded: seeded_note,
|
battle_frames: BTreeMap::new(),
|
||||||
refusals: BTreeMap::new(),
|
battle_starts: BTreeMap::new(),
|
||||||
refusal_run: (None, 0),
|
battle_pads: BTreeMap::new(),
|
||||||
longest_refusal_run: (0, ""),
|
battles: Vec::new(),
|
||||||
|
battle_now: None,
|
||||||
|
payouts_by_kind: BTreeMap::new(),
|
||||||
|
move_starts: (0, 0),
|
||||||
|
wall_seconds: 0.0,
|
||||||
|
seeded: seeded_note,
|
||||||
|
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() {
|
if let Some(layer) = macros.as_mut() {
|
||||||
let ledger = AdapterLedger(&adapter);
|
let ledger = AdapterLedger(&adapter);
|
||||||
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
let _ = layer.observe(&mut emulator, &ledger, agent.network.ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
while agent.network.ms < until {
|
while agent.network.ms < until {
|
||||||
let rewards: Vec<NeuralReward> = payouts
|
let mut parts = Parts {
|
||||||
.iter()
|
agent: &mut agent,
|
||||||
.map(|event| {
|
emulator: &mut emulator,
|
||||||
NeuralReward::with_stimulation(event.value, f64::from(event.stimulation_ms))
|
adapter: &mut adapter,
|
||||||
})
|
ratchet: &mut ratchet,
|
||||||
.collect();
|
macros: macros.as_mut(),
|
||||||
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 held = agent.decoder.current().map(str::to_string);
|
let transition = frame.transition(&mut parts, &mut hunt).expect("a frame");
|
||||||
if held != held_channel {
|
let ms = transition.ms;
|
||||||
held_channel = held;
|
hunt.trace.frames += 1;
|
||||||
blocked_since_ms = ms;
|
for payout in &transition.evaluated.rewards {
|
||||||
}
|
let entry = hunt.trace.payouts_by_kind.entry(payout.kind).or_insert((0, 0.0));
|
||||||
|
|
||||||
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));
|
|
||||||
*entry = (entry.0 + 1, entry.1 + payout.value);
|
*entry = (entry.0 + 1, entry.1 + payout.value);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
use flybrain_gb::MemoryReader;
|
use flybrain_gb::MemoryReader;
|
||||||
|
let trace = &mut hunt.trace;
|
||||||
let fighting =
|
let fighting =
|
||||||
emulator.read8(flybrain_gb::pokemon_red::symbols::ram::wIsInBattle) != 0;
|
parts.emulator.read8(flybrain_gb::pokemon_red::symbols::ram::wIsInBattle) != 0;
|
||||||
let won = payouts.iter().any(|payout| matches!(payout.kind, "battle" | "trainer"));
|
let won = transition
|
||||||
|
.evaluated
|
||||||
|
.rewards
|
||||||
|
.iter()
|
||||||
|
.any(|payout| matches!(payout.kind, "battle" | "trainer"));
|
||||||
match (fighting, trace.battle_now.as_mut()) {
|
match (fighting, trace.battle_now.as_mut()) {
|
||||||
(true, Some(battle)) => {
|
(true, Some(battle)) => {
|
||||||
battle.0 += 1;
|
battle.0 += 1;
|
||||||
|
|
@ -638,19 +617,15 @@ fn run(
|
||||||
(false, None) => {}
|
(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 {
|
if trace_every_ms > 0.0 && ms >= next_trace {
|
||||||
next_trace = ms + trace_every_ms;
|
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};
|
use flybrain_gb::pokemon_red::macros::cartridge::{MacroState, Tile};
|
||||||
// Read before the state borrows the emulator: this is the same call the state makes,
|
// 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.
|
// 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 refusal = flybrain_gb::pokemon_red::state::map_grid(parts.emulator).err();
|
||||||
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(&mut emulator);
|
let mut state = flybrain_gb::pokemon_red::state::PokeState::new(parts.emulator);
|
||||||
let state: &mut dyn MacroState = &mut state;
|
let state: &mut dyn MacroState = &mut state;
|
||||||
let player = state.player();
|
let player = state.player();
|
||||||
let ahead = player.and_then(|player| {
|
let ahead = player.and_then(|player| {
|
||||||
|
|
@ -658,22 +633,18 @@ fn run(
|
||||||
flybrain_gb::pokemon_red::macros::path::target_at(state, ahead)
|
flybrain_gb::pokemon_red::macros::path::target_at(state, ahead)
|
||||||
});
|
});
|
||||||
let ground = grid_line(state, player, refusal);
|
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!(
|
println!(
|
||||||
"trace {:7.2} min scene={scene:<9} player={player:?} ahead={ahead:?}\n {why}\n {ground}",
|
"trace {:7.2} min scene={scene:<9} player={player:?} ahead={ahead:?}\n {why}\n {ground}",
|
||||||
(ms - began_ms) / MINUTE_MS
|
(ms - began_ms) / MINUTE_MS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = adapter.location();
|
let location = frame.location;
|
||||||
if now.is_some() && now != location {
|
|
||||||
location = now;
|
|
||||||
blocked_since_ms = ms;
|
|
||||||
}
|
|
||||||
if let Some((map, x, y)) = 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;
|
run.frames += 1;
|
||||||
if let Some(at) = location {
|
if let Some(at) = location {
|
||||||
run.tiles.insert(at);
|
run.tiles.insert(at);
|
||||||
|
|
@ -685,47 +656,18 @@ fn run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let progress = adapter.progress();
|
let progress = transition.evaluated.progress;
|
||||||
if progress.rank != rank {
|
if progress.rank != rank {
|
||||||
rank = progress.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 boundary = frame.boundary(&mut parts, &progress, ms).expect("the boundary");
|
||||||
let capture_due = safe && u64::from(progress.rank) > ratchet.state.best;
|
if boundary.rollback.is_some() {
|
||||||
let captured = capture_due.then(|| flybrain_gb::ratchet::Snapshot {
|
hunt.trace.recoveries += 1;
|
||||||
game: emulator.export_state().expect("state export"),
|
hunt.running = None;
|
||||||
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 mut trace = hunt.trace;
|
||||||
trace.ended_in = (
|
trace.ended_in = (
|
||||||
macros.as_ref().map_or("", MacroLayer::scene_name),
|
macros.as_ref().map_or("", MacroLayer::scene_name),
|
||||||
adapter.mode().to_string(),
|
adapter.mode().to_string(),
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,25 @@ impl LegacyFrame {
|
||||||
Ok(Transition { ticks, ms, bound, active, executed, audio, evaluated })
|
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`.
|
/// Phase A: brain ticks and the decode, masked to `bound`.
|
||||||
pub fn prepare(
|
pub fn prepare(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -427,12 +446,6 @@ impl LegacyFrame {
|
||||||
Ok(self.take_frame(emulator))
|
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<()> {
|
fn run(&mut self, emulator: &mut Emulator) -> Result<()> {
|
||||||
emulator.set_buttons(self.buttons as u8);
|
emulator.set_buttons(self.buttons as u8);
|
||||||
emulator
|
emulator
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ use flybrain_gb::{
|
||||||
AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, GameAdapter,
|
AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, GameAdapter,
|
||||||
};
|
};
|
||||||
use flysim::config::Config;
|
use flysim::config::Config;
|
||||||
|
use flysim::frame::LegacyFrame;
|
||||||
use flysim::macros::{MacroLayer, macro_layer};
|
use flysim::macros::{MacroLayer, macro_layer};
|
||||||
use flysim::snapshot::MacroMode;
|
use flysim::snapshot::MacroMode;
|
||||||
|
|
||||||
|
|
@ -92,6 +93,8 @@ struct Run {
|
||||||
gb: Emulator,
|
gb: Emulator,
|
||||||
adapter: PokemonRedReward,
|
adapter: PokemonRedReward,
|
||||||
layer: MacroLayer,
|
layer: MacroLayer,
|
||||||
|
/// The stream's frame (`flysim::frame::LegacyFrame`), behind the stub readout.
|
||||||
|
legacy: LegacyFrame,
|
||||||
decoder: PopulationDecoder,
|
decoder: PopulationDecoder,
|
||||||
channels: Vec<&'static str>,
|
channels: Vec<&'static str>,
|
||||||
ms: f64,
|
ms: f64,
|
||||||
|
|
@ -124,6 +127,7 @@ impl Run {
|
||||||
gb,
|
gb,
|
||||||
adapter,
|
adapter,
|
||||||
layer,
|
layer,
|
||||||
|
legacy: LegacyFrame::new(),
|
||||||
decoder,
|
decoder,
|
||||||
channels,
|
channels,
|
||||||
ms: 0.0,
|
ms: 0.0,
|
||||||
|
|
@ -161,18 +165,14 @@ impl Run {
|
||||||
};
|
};
|
||||||
let bound = self.layer.bound_channels();
|
let bound = self.layer.bound_channels();
|
||||||
let active = self.decoder.decode_bound(&rates(hot), self.ms, false, None, Some(&bound));
|
let active = self.decoder.decode_bound(&rates(hot), self.ms, false, None, Some(&bound));
|
||||||
let mask = {
|
self.legacy.execute(Some(&mut self.layer), &active, 0, self.ms, &mut self.gb, &self.adapter);
|
||||||
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.ms += MS_PER_FRAME;
|
self.ms += MS_PER_FRAME;
|
||||||
self.frame += 1;
|
self.frame += 1;
|
||||||
let ms = self.ms;
|
let evaluated = self
|
||||||
self.payouts.extend(self.adapter.sample(&mut self.gb, ms));
|
.legacy
|
||||||
let ledger = AdapterLedger(&self.adapter);
|
.stub_advance(Some(&mut self.layer), &mut self.gb, &mut self.adapter, self.ms)
|
||||||
let _ = self.layer.observe(&mut self.gb, &ledger, ms);
|
.expect("a frame should complete");
|
||||||
|
self.payouts.extend(evaluated.rewards);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn catches(&self) -> Vec<&RewardEvent> {
|
fn catches(&self) -> Vec<&RewardEvent> {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ use flybrain_gb::{
|
||||||
AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, GameAdapter, buttons,
|
AdapterLedger, DEFAULT_AUDIO_FRAMES, DEFAULT_AUDIO_FREQUENCY, Emulator, GameAdapter, buttons,
|
||||||
};
|
};
|
||||||
use flysim::config::Config;
|
use flysim::config::Config;
|
||||||
|
use flysim::frame::LegacyFrame;
|
||||||
use flysim::macros::{MacroLayer, macro_layer};
|
use flysim::macros::{MacroLayer, macro_layer};
|
||||||
use flysim::snapshot::MacroMode;
|
use flysim::snapshot::MacroMode;
|
||||||
|
|
||||||
|
|
@ -147,6 +148,8 @@ struct Run {
|
||||||
gb: Emulator,
|
gb: Emulator,
|
||||||
adapter: PokemonRedReward,
|
adapter: PokemonRedReward,
|
||||||
layer: MacroLayer,
|
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.
|
/// The readout under test: the shipping decoder, fed by hand.
|
||||||
decoder: PopulationDecoder,
|
decoder: PopulationDecoder,
|
||||||
/// The macro channels, in the decoder's own order, for the rotation.
|
/// The macro channels, in the decoder's own order, for the rotation.
|
||||||
|
|
@ -402,6 +405,7 @@ impl Run {
|
||||||
gb,
|
gb,
|
||||||
adapter,
|
adapter,
|
||||||
layer,
|
layer,
|
||||||
|
frame: LegacyFrame::new(),
|
||||||
decoder,
|
decoder,
|
||||||
channels,
|
channels,
|
||||||
ms,
|
ms,
|
||||||
|
|
@ -520,6 +524,7 @@ impl Run {
|
||||||
gb,
|
gb,
|
||||||
adapter,
|
adapter,
|
||||||
layer,
|
layer,
|
||||||
|
frame: LegacyFrame::new(),
|
||||||
decoder,
|
decoder,
|
||||||
channels,
|
channels,
|
||||||
ms,
|
ms,
|
||||||
|
|
@ -815,9 +820,15 @@ impl Run {
|
||||||
self.talk_on_pad = talk_bound;
|
self.talk_on_pad = talk_bound;
|
||||||
let active =
|
let active =
|
||||||
self.decoder.decode_bound(&rates(hot), self.ms, false, None, Some(&bound));
|
self.decoder.decode_bound(&rates(hot), self.ms, false, None, Some(&bound));
|
||||||
let (mask, started, blocked, done) = {
|
let (started, blocked, done) = {
|
||||||
let ledger = AdapterLedger(&self.adapter);
|
let decision = self.frame.execute(
|
||||||
let decision = self.layer.decide(&active, 0, self.ms, &mut self.gb, &ledger);
|
Some(&mut self.layer),
|
||||||
|
&active,
|
||||||
|
0,
|
||||||
|
self.ms,
|
||||||
|
&mut self.gb,
|
||||||
|
&self.adapter,
|
||||||
|
);
|
||||||
let started: Vec<&'static str> = decision
|
let started: Vec<&'static str> = decision
|
||||||
.events
|
.events
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -840,7 +851,7 @@ impl Run {
|
||||||
})
|
})
|
||||||
.map(|event| event.name)
|
.map(|event| event.name)
|
||||||
.collect();
|
.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
|
// 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.
|
// another that starts on the same frame are not confused for one another.
|
||||||
|
|
@ -963,15 +974,10 @@ impl Run {
|
||||||
let (x, y) = self.tile();
|
let (x, y) = self.tile();
|
||||||
self.started_at = Some((self.map(), x, y));
|
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;
|
self.ms += MS_PER_FRAME;
|
||||||
let ms = self.ms;
|
self.frame
|
||||||
self.adapter.sample(&mut self.gb, ms);
|
.stub_advance(Some(&mut self.layer), &mut self.gb, &mut self.adapter, self.ms)
|
||||||
{
|
.expect("a frame should complete");
|
||||||
let ledger = AdapterLedger(&self.adapter);
|
|
||||||
let _ = self.layer.observe(&mut self.gb, &ledger, ms);
|
|
||||||
}
|
|
||||||
// Battle boundaries, after the frame: what a battle cost in macros, and whether it ended.
|
// Battle boundaries, after the frame: what a battle cost in macros, and whether it ended.
|
||||||
let now_in_battle = self.in_battle() != 0;
|
let now_in_battle = self.in_battle() != 0;
|
||||||
match (self.was_in_battle, now_in_battle) {
|
match (self.was_in_battle, now_in_battle) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue