bot.rsannotatedbot.rssource141 lines · 5.8 KB · raw
1//! What the bot playing the game says about itself: the zbanks/alttp C bot
2//! (`packages/zbanks`), read from its own globals once a UI pass.
3//!
4//! Plain data in, so this crate needs neither the bot nor its C and still
5//! compiles for wasm; the app fills a [`Bot`] from `zbanks::Report`.
6
7use egui::{FontId, RichText};
8
9use crate::palette::{FAINT, GOAL, GOOD, GREY, PLAIN, RED, SECTION};
10
11/// One goal on the bot's goal list (`ap_plan.c`'s `struct ap_goal`).
12pub struct Goal<'a> {
13    /// `GOAL_*` without the prefix: EXPLORE, CHEST, PICKUP, NPC, ITEM, SCRIPT.
14    pub kind: &'a str,
15    /// The node the goal is about, in the bot's own naming.
16    pub node: &'a str,
17    /// The screen that node is on.
18    pub screen: &'a str,
19    pub attempts: i32,
20    /// The score the planner last gave it, as words when it is not a cost
21    /// ("complete", "unsatisfiable", "over limit" - `zbanks::score_words`).
22    pub score: String,
23    /// Whether that score is one the planner will not pick.
24    pub out_of_reach: bool,
25    pub complete: bool,
26}
27
28pub struct Bot<'a> {
29    /// The frame last passed to `ap_tick`.
30    pub frame: u32,
31    /// `ap_info_string`: the line upstream's Snes9x drew over the picture.
32    pub info: &'a str,
33    /// The task list, current task first, in `ap_print_task`'s words.
34    pub tasks: &'a [String],
35    pub goals: &'a [Goal<'a>],
36    /// All goals the bot has, of which `goals` are the first.
37    pub goal_count: usize,
38    pub link: (u16, u16),
39    /// `assert_bp` failures so far (each a trap the host stepped over, as gdb
40    /// would), and the frame of the last.
41    pub traps: u64,
42    pub last_trap_frame: u64,
43    /// `base()` requests that mapped to nothing; should stay 0.
44    pub stray_reads: u64,
45    /// Goals the bot gave up on, waiting for Link's possessions to change,
46    /// and how many the host has put back so far.
47    pub given_up: usize,
48    pub retried: u64,
49    /// Set by the host's stall detector (`packages/zbanks/src/stall.rs`):
50    /// the bot has not chosen a goal, or has not moved, for its threshold's
51    /// worth of frames, or the planner has given up for good. `None` means
52    /// not stalled - never omitted quietly, so the panel cannot look normal
53    /// while the bot has stopped playing.
54    pub stalled: Option<&'a str>,
55    /// The host's own recovery from a stall (`packages/zbanks/src/recovery.rs`):
56    /// the last attempt's line, and whether recovery has given up for good
57    /// (shown in red, like `stalled`, instead of the ordinary attention
58    /// colour) - once true it stays true for the life of the process, same
59    /// as `stalled`'s own `ManualMode` used to before recovery could clear
60    /// it.
61    pub recovery: Option<(&'a str, bool)>,
62    /// Held on the keyboard: the bot steps aside (upstream alttp.c:57).
63    pub paused_by_x: bool,
64}
65
66/// A small header used by more than one panel in this crate. `pub(crate)`
67/// so `jev.rs`'s "History" section can use the exact same look.
68pub(crate) fn section(ui: &mut egui::Ui, title: &str) {
69    ui.add_space(4.0);
70    ui.label(RichText::new(title).color(SECTION).strong());
71}
72
73/// The bot's line, its tasks and its goals.
74pub fn show(ui: &mut egui::Ui, bot: &Bot) {
75    ui.heading("Bot");
76    if let Some(stalled) = bot.stalled {
77        ui.add(egui::Label::new(RichText::new(stalled).color(RED).strong()).wrap());
78    }
79    if let Some((line, gave_up)) = bot.recovery {
80        let colour = if gave_up { RED } else { GOAL };
81        ui.add(egui::Label::new(RichText::new(line).color(colour).strong()).wrap());
82    }
83    ui.label(RichText::new("zbanks/alttp, C").color(GREY));
84    let info = if bot.info.is_empty() { "(nothing yet)" } else { bot.info };
85    ui.add(egui::Label::new(RichText::new(info).font(FontId::monospace(12.0)).color(GOAL)).wrap());
86    if bot.paused_by_x {
87        ui.label(RichText::new("X held: the bot steps aside, the pad is yours").color(RED));
88    }
89    let mut facts = format!(
90        "frame {}   link {:#06x},{:#06x}   goals {}",
91        bot.frame, bot.link.0, bot.link.1, bot.goal_count
92    );
93    if bot.traps > 0 {
94        facts.push_str(&format!("   traps {} (last frame {})", bot.traps, bot.last_trap_frame));
95    }
96    if bot.given_up > 0 || bot.retried > 0 {
97        facts.push_str(&format!("   given up {} (put back {})", bot.given_up, bot.retried));
98    }
99    ui.label(RichText::new(facts).color(GREY));
100    if bot.stray_reads > 0 {
101        ui.label(RichText::new(format!("{} reads of unmapped memory", bot.stray_reads)).color(RED));
102    }
103
104    egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
105        section(ui, "Tasks");
106        if bot.tasks.is_empty() {
107            ui.label(RichText::new("none").color(GREY));
108        }
109        for (i, task) in bot.tasks.iter().enumerate() {
110            let colour = if i == 0 { GOOD } else { PLAIN };
111            ui.add(
112                egui::Label::new(RichText::new(task).font(FontId::monospace(11.0)).color(colour))
113                    .wrap(),
114            );
115        }
116        section(ui, "Goals");
117        for goal in bot.goals {
118            let colour = if goal.complete {
119                GOOD
120            } else if goal.out_of_reach {
121                FAINT
122            } else {
123                PLAIN
124            };
125            let line = format!(
126                "{:<7} {:>12}  {}  on {}{}",
127                goal.kind,
128                goal.score,
129                goal.node,
130                goal.screen,
131                if goal.attempts > 0 { format!("  ({} tries)", goal.attempts) } else { String::new() },
132            );
133            ui.add(egui::Label::new(RichText::new(line).font(FontId::monospace(11.0)).color(colour)).wrap());
134        }
135        if bot.goal_count > bot.goals.len() {
136            ui.label(
137                RichText::new(format!("... and {} more", bot.goal_count - bot.goals.len())).color(GREY),
138            );
139        }
140    });
141}