diff --git a/fish/src/bot.rs b/fish/src/bot.rs index 4bfc59b..41919aa 100644 --- a/fish/src/bot.rs +++ b/fish/src/bot.rs @@ -2,10 +2,10 @@ //! anytime algorithm that will provide a suggestion for the next move. It may be //! repeatedly polled by the `think` method in order to attempt to improve the suggestion. -use core::ops::Range; - +use alloc::boxed::Box; use alloc::collections::BinaryHeap; use alloc::vec::Vec; +use core::ops::Range; use mino::matrix::Mat; use mino::srs::{Piece, Queue}; @@ -14,18 +14,32 @@ mod trans; use crate::bot::node::{Node, RawNodePtr}; use crate::bot::trans::TransTable; -use crate::eval::{features, Weights}; +use crate::eval::{features, Features, Weights}; pub(crate) use bumpalo::Bump as Arena; /// Encompasses an instance of the algorithm. pub struct Bot { - iters: u32, evaluator: Evaluator, trans: TransTable, algorithm: SegmentedAStar, // IMPORTANT: `arena` must occur after `algorithm` so that it is dropped last. arena: Arena, + iters: u32, + metrics: Option>, +} + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Metrics { + pub start_features: Features, + pub start_heuristic: i32, + pub end_features: Features, + pub end_heuristic: i32, + pub end_rating: (bool, i32), + pub end_iteration: u32, + // TODO(?) memory usage metrics + // TODO(?) transposition table metrics + // TODO(?) A* metrics } impl Bot { @@ -37,11 +51,12 @@ impl Bot { let trans = TransTable::new(); let algorithm = SegmentedAStar::new(root); Self { - iters: 0, evaluator, trans, algorithm, arena, + iters: 0, + metrics: None, } } @@ -66,12 +81,19 @@ impl Bot { &self.evaluator, &mut self.iters, ); + if did_update { tracing::debug!( "new suggestion @ {}: {:?}", self.iters, - self.algorithm.best().unwrap(), + self.algorithm.best(), ); + + if let Some(metrics) = self.metrics.as_deref_mut() { + let weights = &self.evaluator.weights; + let start = self.algorithm.best(); + metrics.updated_best(weights, start, self.iters); + } } } } @@ -86,7 +108,20 @@ impl Bot { /// - `think` has not been called enough times to provide an initial suggestion. /// - there are no valid placements for the initial state pub fn suggest(&self) -> Option { - self.algorithm.best().and_then(|node| node.root_placement()) + self.algorithm.best().root_placement() + } + + /// Start collecting metrics about the search algorithm. Uses the current suggestion + /// as the "start" of the metrics data. + pub fn start_instrumenting(&mut self) { + let weights = &self.evaluator.weights; + let start = self.algorithm.best(); + self.metrics = Some(Metrics::new(weights, start, self.iters).into()); + } + + /// Returns the current metrics, if enabled by `start_instrumenting`. + pub fn metrics(&self) -> Option { + self.metrics.as_deref().cloned() } } @@ -127,6 +162,33 @@ impl Evaluator { } } +impl Metrics { + fn new(weights: &Weights, start: &Node, iters: u32) -> Self { + let features = features(start.matrix(), 0); + let heuristic = features.evaluate(weights); + let rating = start.rating(); + Self { + start_features: features, + start_heuristic: heuristic, + end_features: features, + end_heuristic: heuristic, + end_rating: rating, + end_iteration: iters, + } + } + + #[cold] + fn updated_best(&mut self, weights: &Weights, node: &Node, iters: u32) { + let features = features(node.matrix(), 0); + let heuristic = features.evaluate(weights); + let rating = node.rating(); + self.end_features = features; + self.end_heuristic = heuristic; + self.end_rating = rating; + self.end_iteration = iters; + } +} + // This implements an algorithm that is very similar to A* but has a slight // modification. Rather than one big open set, there are separate sets at each depth of // the search. After picking a node from one open set and expanding its children into the @@ -154,7 +216,7 @@ impl Evaluator { struct SegmentedAStar { open: Vec>, depth: usize, - best: Option, + best: RawNodePtr, } impl SegmentedAStar { @@ -165,12 +227,12 @@ impl SegmentedAStar { Self { open, depth: 0, - best: None, + best: root.into(), } } - fn best(&self) -> Option<&Node> { - self.best.map(|node| unsafe { node.as_node() }) + fn best(&self) -> &Node { + unsafe { self.best.as_node() } } fn step( @@ -229,9 +291,10 @@ impl SegmentedAStar { Ok(work) } + #[cold] fn backup(&mut self, cand: &Node) -> bool { - if self.best().map_or(true, |best| cand.is_better(best)) { - self.best = Some(cand.into()); + if cand.is_better(self.best()) { + self.best = cand.into(); true } else { false diff --git a/fish/src/bot/node.rs b/fish/src/bot/node.rs index ead3705..0020cfb 100644 --- a/fish/src/bot/node.rs +++ b/fish/src/bot/node.rs @@ -70,10 +70,16 @@ impl Node { unsafe { self.queue.as_queue() } } + pub fn rating(&self) -> (bool, i32) { + self.rating + } + + // TODO: move this function to `bot.algorithm` pub fn is_better(&self, other: &Node) -> bool { self.rating > other.rating } + // TODO: move this function to `bot.algorithm` pub fn is_terminal(&self) -> bool { self.queue().is_empty() || self.rating.0 } @@ -146,7 +152,7 @@ impl core::fmt::Debug for Node { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "Node {{ ")?; match self.rating { - (false, h) => write!(f, "heuristic: {}", h), + (false, h) => write!(f, "rating: {}", h), (true, n) => write!(f, "solution: {}", -n), }?; if let Some(pc) = self.root_placement() {