diff --git a/fish/src/bot.rs b/fish/src/bot.rs index e1ce0a1..4bfc59b 100644 --- a/fish/src/bot.rs +++ b/fish/src/bot.rs @@ -2,6 +2,8 @@ //! 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::collections::BinaryHeap; use alloc::vec::Vec; use mino::matrix::Mat; @@ -103,34 +105,25 @@ impl Evaluator { } } - fn evaluate(&self, mat: &Mat, queue: Queue<'_>) -> i32 { - // FIXME: the old blockfish has two special edge cases for rating nodes that is - // not done here. - // - // 1. nodes that reach the bottom of the board early ("solutions") are highly - // prioritized. this is done by using the piece count *as the rating* in order to - // force it to be extremely low, as well as sorting solutions by # of pieces in - // case there are multiple. according to frey, this probably causes blockfish to - // greed out in various scenarios where it sees a path to the bottom but it is not - // actually the end of the race. part of the issue is of course that it isn't - // communicated to blockfish whether or not the bottom of the board is actually - // the end of the race, but also that the intermediate steps to get to the bottom - // may be suboptimal placements when it isn't. - // - // 2. blockfish would actually average the last two evaluations and use that as - // the final rating. this is meant as a concession for the fact that the last - // placement made by the bot is not actually a placement we are required to make, - // since in reality there is going to be the opportunity to hold the final piece - // and use something else instead. so the 2nd to last rating is important in cases - // where the last piece leads to suboptimal board states which may be able to be - // avoided by holding the last piece. i think this improves the performance only - // slightly, but it is also a bit of a hack that deserves further consideration. - + fn evaluate(&self, mat: &Mat, queue: Queue<'_>, cleared: &Range) -> (bool, i32) { let pcnt = self.root_queue_len.saturating_sub(queue.len()); + + if self.greed() && cleared.contains(&0) { + // cleared the bottom row of the matrix, which must be the last line of cheese + // in the race. piece count is negated so that less pieces is better (larger + // value). + return (true, -(pcnt as i32)); + } + let score = features(mat, pcnt).evaluate(&self.weights); - // larger (i.e., further below the root score) is better - self.root_score - score + // larger (further below the root score) is better + (false, self.root_score - score) + } + + fn greed(&self) -> bool { + // TODO: make this parameter configurable on `Bot` initialization + true } } @@ -224,10 +217,10 @@ impl SegmentedAStar { } let mut work = 0; - let evaluate = |mat: &Mat, queue: Queue<'_>| { + let evaluate = |mat: &Mat, queue: Queue<'_>, clr: &Range| { // each evaluated board state = +1 unit work work += 1; - eval.evaluate(mat, queue) + eval.evaluate(mat, queue, clr) }; let expanded = cand.expand(arena, trans, evaluate); diff --git a/fish/src/bot/node.rs b/fish/src/bot/node.rs index 1139528..ead3705 100644 --- a/fish/src/bot/node.rs +++ b/fish/src/bot/node.rs @@ -1,5 +1,7 @@ //! Graph data structures used by `Bot` in its search algorithm. +use core::ops::Range; + use mino::matrix::{Mat, MatBuf}; use mino::srs::{Piece, PieceType, Queue}; @@ -13,7 +15,7 @@ pub(crate) struct Node { matrix: *const Mat, queue: RawQueue, edge: Option, - rating: i32, + rating: (bool, i32), // currently there is no need to store a node's children, but maybe this could change // in the future. } @@ -38,7 +40,8 @@ impl Node { pub fn alloc_root<'a>(arena: &'a Arena, matrix: &Mat, queue: Queue<'_>) -> &'a Self { let matrix = copy_matrix(arena, matrix); let queue = copy_queue(arena, queue); - Node::alloc(arena, matrix, queue, i32::MIN, None) + let rating = (false, i32::MIN); + Node::alloc(arena, matrix, queue, rating, None) } // `matrix` and `queue` must be allocated inside `arena` @@ -46,7 +49,7 @@ impl Node { arena: &'a Arena, matrix: &'a Mat, queue: Queue<'a>, - rating: i32, + rating: (bool, i32), edge: Option, ) -> &'a Self { let matrix = matrix as *const Mat; @@ -72,8 +75,7 @@ impl Node { } pub fn is_terminal(&self) -> bool { - // TODO: additional terminal-node conditions e.g. clears last row of garbage - self.queue().is_empty() + self.queue().is_empty() || self.rating.0 } /// Get the initial placement made after the root node which eventually arrives at @@ -99,7 +101,7 @@ impl Node { mut evaluate: E, ) -> impl Iterator + 'a where - E: FnMut(&Mat, Queue<'_>) -> i32 + 'a, + E: FnMut(&Mat, Queue<'_>, &Range) -> (bool, i32) + 'a, { let placements = self.queue().reachable().flat_map(|ty| { let locs = find_locations(self.matrix(), ty); @@ -111,9 +113,7 @@ impl Node { placements.filter_map(move |placement| { matrix.copy_from(self.matrix()); placement.cells().fill(&mut matrix); - matrix.clear_lines(); - // TODO: the above call returns useful information about if this placement is - // a combo & does it clear the bottom row of garbage + let cleared = matrix.clear_lines(); let queue = self.queue().remove(placement.ty); trans @@ -125,7 +125,7 @@ impl Node { // allocated on the arena, and this queue just aliases pointers // into self.queue.next queue, - evaluate(&matrix, queue), + evaluate(&matrix, queue, &cleared), Some(Edge { placement, parent: self.into(), @@ -144,7 +144,11 @@ impl Node { impl core::fmt::Debug for Node { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "Node {{ rating: {}", self.rating)?; + write!(f, "Node {{ ")?; + match self.rating { + (false, h) => write!(f, "heuristic: {}", h), + (true, n) => write!(f, "solution: {}", -n), + }?; if let Some(pc) = self.root_placement() { write!(f, ", root_placement: {:?}", pc)?; }