1//! Reconstruct the BOT's own state at a frame by re-running a fresh one 2//! over the logged history: exact, because the bot is a pure function of 3//! RAM, the pad, `rand()` and the goal-choice hook's picks - all fixed by 4//! the log (`research/replay-timeline.md`, "Re-running the bot over the log 5//! is exact"). This is what "run the bot from here" needs, since a 6//! `zbanks::Bot`'s state is C globals that no snapshot can capture 7//! (`packages/zbanks/CLAUDE.md`: one bot per process) - `crate::seek` 8//! reconstructs the CONSOLE quickly for scrubbing, but the bot's own map, 9//! goals and item-location deductions only come back by actually ticking it 10//! through everything that happened. 11//! 12//! Runs a bounded slice of frames per [`Rebuilder::step`] rather than all at 13//! once, so a caller with a UI can show progress ("rebuilding bot: 14//! 12,000/36,000 frames") instead of blocking. 15//! 16//! Frame numbering here is LOCAL to the branch being rebuilt, and lines up 17//! with the fresh `zbanks::Bot`'s own internal frame counter by 18//! construction: a recording always starts exactly when a bot does 19//! (`crate::recorder`'s own CLAUDE.md note), so entry `i` was recorded on 20//! the tick where the ORIGINAL bot's frame counter was also `i`, and 21//! feeding entries `0..` to a fresh bot in order reproduces that exactly - 22//! no frame number needs to be threaded through the goal-choice hook itself. 23 24use std::cell::Cell; 25use std::rc::Rc; 26 27use console::Console; 28use zbanks::{Bot, GoalChooser, GoalOption, Situation, States}; 29 30use crate::format::{FrameEntry, Pick}; 31 32/// Feeds back exactly the pick the ORIGINAL run's hook used, on the tick it 33/// used it - never asks Jev, never makes a fresh choice. [`Rebuilder::step`] 34/// sets `pending` immediately before each tick that has a logged decision. 35struct ReplayChooser { 36 pending: Rc<Cell<Option<Pick>>>, 37} 38 39impl GoalChooser for ReplayChooser { 40 fn choose(&mut self, _frame: u32, _here: &Situation, options: &[GoalOption]) -> Option<usize> { 41 match self.pending.take() { 42 Some(Pick::Index(i)) if (i as usize) < options.len() => Some(i as usize), 43 _ => None, 44 } 45 } 46} 47 48pub struct Progress { 49 pub done: u64, 50 pub total: u64, 51 pub finished: bool, 52} 53 54pub struct Rebuilder { 55 entries: Vec<FrameEntry>, 56 index: usize, 57 pending: Rc<Cell<Option<Pick>>>, 58} 59 60impl Rebuilder { 61 /// `entries` are the flattened, in-order [`FrameEntry`] values from a 62 /// branch's own frame 1 up to (and stopping at) the target frame - the 63 /// caller (walking the branch chain, as `crate::seek` does) has already 64 /// picked out exactly the slice that matters. Returns the driver plus 65 /// the [`GoalChooser`] to install on a freshly started `Bot` with 66 /// `Bot::set_goal_chooser` BEFORE the first call to [`Self::step`]. 67 pub fn new(entries: Vec<FrameEntry>) -> (Self, Box<dyn GoalChooser>) { 68 let pending = Rc::new(Cell::new(None)); 69 let chooser = ReplayChooser { pending: pending.clone() }; 70 (Self { entries, index: 0, pending }, Box::new(chooser)) 71 } 72 73 pub fn total(&self) -> u64 { 74 self.entries.len() as u64 75 } 76 77 /// Run up to `budget` more logged frames through `bot`. `states` is the 78 /// same [`States`] the bot was started with (it only ever asks for 79 /// "home"/"hpegs" once, at `Bot::start`, but the trait object must stay 80 /// alive for the type to name). 81 pub fn step(&mut self, bot: &mut Bot, console: &mut Console, states: &mut dyn States, budget: u32) -> Progress { 82 let mut ran = 0; 83 while self.index < self.entries.len() && ran < budget { 84 let entry = &self.entries[self.index]; 85 self.pending.set(entry.decision); 86 let pad = bot.tick(console, states, entry.human_pad); 87 zbanks::apply_pad(console, pad); 88 console.run_frame().expect("running a logged frame during rebuild"); 89 self.index += 1; 90 ran += 1; 91 } 92 Progress { done: self.index as u64, total: self.entries.len() as u64, finished: self.index >= self.entries.len() } 93 } 94} 95 96#[cfg(test)] 97mod tests { 98 // Rebuilder drives a real `zbanks::Bot` over a real `console::Console`, 99 // and `zbanks::Bot::start` panics on a second call in the same process 100 // (packages/zbanks/CLAUDE.md, "one bot per process") - exactly the 101 // constraint that makes a REBUILD a re-exec in `apps/native`, not an 102 // in-process operation. That rules out a from-scratch unit test here 103 // the same way `crate::seek`'s does; `apps/replay-probe` is where a 104 // real rebuild gets exercised end to end, once it grows a `--rebuild` 105 // mode. 106 use super::*; 107 108 #[test] 109 fn a_replay_chooser_reads_and_clears_its_pending_pick() { 110 let pending = Rc::new(Cell::new(None)); 111 let mut chooser = ReplayChooser { pending: pending.clone() }; 112 let options = [GoalOption { 113 kind: String::new(), 114 node: String::new(), 115 screen: String::new(), 116 screen_id: 0, 117 indoors: false, 118 direction: 0, 119 sprite_type: 0, 120 sprite_subtype: 0, 121 item: String::new(), 122 needs: String::new(), 123 score: 0, 124 distance: 0, 125 attempts: 0, 126 adjacent_known: false, 127 at: (0, 0), 128 screen_bounds: ((0, 0), (0, 0)), 129 via: String::new(), 130 via_direction: 0, 131 screens_on_path: 0, 132 on_way_to: 0, 133 }]; 134 let here = Situation::default(); 135 136 // Nothing pending: upstream's own pick. 137 assert_eq!(chooser.choose(0, &here, &options), None); 138 139 pending.set(Some(Pick::Index(0))); 140 assert_eq!(chooser.choose(0, &here, &options), Some(0)); 141 // Consumed: asking again with nothing newly pending falls back. 142 assert_eq!(chooser.choose(0, &here, &options), None); 143 144 pending.set(Some(Pick::Upstream)); 145 assert_eq!(chooser.choose(0, &here, &options), None); 146 147 // An index the options no longer cover (recorded against a longer 148 // list than this call offers) also falls back, rather than panicking. 149 pending.set(Some(Pick::Index(5))); 150 assert_eq!(chooser.choose(0, &here, &options), None); 151 } 152}