recognize clearing the bottom matrix row in evaluator

This commit is contained in:
tali 2023-04-16 16:34:45 -04:00
parent 082fa62add
commit 1784647146
2 changed files with 35 additions and 38 deletions

View File

@ -2,6 +2,8 @@
//! anytime algorithm that will provide a suggestion for the next move. It may be //! 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. //! repeatedly polled by the `think` method in order to attempt to improve the suggestion.
use core::ops::Range;
use alloc::collections::BinaryHeap; use alloc::collections::BinaryHeap;
use alloc::vec::Vec; use alloc::vec::Vec;
use mino::matrix::Mat; use mino::matrix::Mat;
@ -103,34 +105,25 @@ impl Evaluator {
} }
} }
fn evaluate(&self, mat: &Mat, queue: Queue<'_>) -> i32 { fn evaluate(&self, mat: &Mat, queue: Queue<'_>, cleared: &Range<i16>) -> (bool, 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.
let pcnt = self.root_queue_len.saturating_sub(queue.len()); 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); let score = features(mat, pcnt).evaluate(&self.weights);
// larger (i.e., further below the root score) is better // larger (further below the root score) is better
self.root_score - score (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 mut work = 0;
let evaluate = |mat: &Mat, queue: Queue<'_>| { let evaluate = |mat: &Mat, queue: Queue<'_>, clr: &Range<i16>| {
// each evaluated board state = +1 unit work // each evaluated board state = +1 unit work
work += 1; work += 1;
eval.evaluate(mat, queue) eval.evaluate(mat, queue, clr)
}; };
let expanded = cand.expand(arena, trans, evaluate); let expanded = cand.expand(arena, trans, evaluate);

View File

@ -1,5 +1,7 @@
//! Graph data structures used by `Bot` in its search algorithm. //! Graph data structures used by `Bot` in its search algorithm.
use core::ops::Range;
use mino::matrix::{Mat, MatBuf}; use mino::matrix::{Mat, MatBuf};
use mino::srs::{Piece, PieceType, Queue}; use mino::srs::{Piece, PieceType, Queue};
@ -13,7 +15,7 @@ pub(crate) struct Node {
matrix: *const Mat, matrix: *const Mat,
queue: RawQueue, queue: RawQueue,
edge: Option<Edge>, edge: Option<Edge>,
rating: i32, rating: (bool, i32),
// currently there is no need to store a node's children, but maybe this could change // currently there is no need to store a node's children, but maybe this could change
// in the future. // in the future.
} }
@ -38,7 +40,8 @@ impl Node {
pub fn alloc_root<'a>(arena: &'a Arena, matrix: &Mat, queue: Queue<'_>) -> &'a Self { pub fn alloc_root<'a>(arena: &'a Arena, matrix: &Mat, queue: Queue<'_>) -> &'a Self {
let matrix = copy_matrix(arena, matrix); let matrix = copy_matrix(arena, matrix);
let queue = copy_queue(arena, queue); 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` // `matrix` and `queue` must be allocated inside `arena`
@ -46,7 +49,7 @@ impl Node {
arena: &'a Arena, arena: &'a Arena,
matrix: &'a Mat, matrix: &'a Mat,
queue: Queue<'a>, queue: Queue<'a>,
rating: i32, rating: (bool, i32),
edge: Option<Edge>, edge: Option<Edge>,
) -> &'a Self { ) -> &'a Self {
let matrix = matrix as *const Mat; let matrix = matrix as *const Mat;
@ -72,8 +75,7 @@ impl Node {
} }
pub fn is_terminal(&self) -> bool { pub fn is_terminal(&self) -> bool {
// TODO: additional terminal-node conditions e.g. clears last row of garbage self.queue().is_empty() || self.rating.0
self.queue().is_empty()
} }
/// Get the initial placement made after the root node which eventually arrives at /// Get the initial placement made after the root node which eventually arrives at
@ -99,7 +101,7 @@ impl Node {
mut evaluate: E, mut evaluate: E,
) -> impl Iterator<Item = &'a Node> + 'a ) -> impl Iterator<Item = &'a Node> + 'a
where where
E: FnMut(&Mat, Queue<'_>) -> i32 + 'a, E: FnMut(&Mat, Queue<'_>, &Range<i16>) -> (bool, i32) + 'a,
{ {
let placements = self.queue().reachable().flat_map(|ty| { let placements = self.queue().reachable().flat_map(|ty| {
let locs = find_locations(self.matrix(), ty); let locs = find_locations(self.matrix(), ty);
@ -111,9 +113,7 @@ impl Node {
placements.filter_map(move |placement| { placements.filter_map(move |placement| {
matrix.copy_from(self.matrix()); matrix.copy_from(self.matrix());
placement.cells().fill(&mut matrix); placement.cells().fill(&mut matrix);
matrix.clear_lines(); let cleared = 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 queue = self.queue().remove(placement.ty); let queue = self.queue().remove(placement.ty);
trans trans
@ -125,7 +125,7 @@ impl Node {
// allocated on the arena, and this queue just aliases pointers // allocated on the arena, and this queue just aliases pointers
// into self.queue.next // into self.queue.next
queue, queue,
evaluate(&matrix, queue), evaluate(&matrix, queue, &cleared),
Some(Edge { Some(Edge {
placement, placement,
parent: self.into(), parent: self.into(),
@ -144,7 +144,11 @@ impl Node {
impl core::fmt::Debug for Node { impl core::fmt::Debug for Node {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 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() { if let Some(pc) = self.root_placement() {
write!(f, ", root_placement: {:?}", pc)?; write!(f, ", root_placement: {:?}", pc)?;
} }