recognize clearing the bottom matrix row in evaluator
This commit is contained in:
parent
082fa62add
commit
1784647146
|
@ -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);
|
||||||
|
|
|
@ -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)?;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue