add Features and Weights types to fish::eval

This commit is contained in:
tali 2023-04-15 18:25:27 -04:00
parent 32f66bb423
commit 3445f9e44a
4 changed files with 264 additions and 225 deletions

View File

@ -10,7 +10,7 @@ use mino::srs::{Piece, Queue};
mod node; mod node;
use self::node::{Node, RawNodePtr}; use self::node::{Node, RawNodePtr};
use crate::eval::evaluate; use crate::eval::{features, Weights};
pub(crate) use bumpalo::Bump as Arena; pub(crate) use bumpalo::Bump as Arena;
@ -25,10 +25,10 @@ pub struct Bot {
impl Bot { impl Bot {
/// Constructs a new bot from the given initial state (matrix and queue). /// Constructs a new bot from the given initial state (matrix and queue).
// TODO: specify weights // TODO: specify weights
pub fn new(matrix: &Mat, queue: Queue<'_>) -> Self { pub fn new(weights: &Weights, matrix: &Mat, queue: Queue<'_>) -> Self {
let arena = bumpalo::Bump::new(); let arena = bumpalo::Bump::new();
let root = Node::alloc_root(&arena, matrix, queue); let root = Node::alloc_root(&arena, matrix, queue);
let evaluator = Evaluator::new(root); let evaluator = Evaluator::new(weights, root);
let algorithm = SegmentedAStar::new(root); let algorithm = SegmentedAStar::new(root);
Self { Self {
evaluator, evaluator,
@ -56,21 +56,21 @@ impl Bot {
struct Evaluator { struct Evaluator {
// TODO: weights // TODO: weights
weights: Weights,
root_score: i32, root_score: i32,
root_queue_len: usize, root_queue_len: usize,
} }
impl Evaluator { impl Evaluator {
fn new(root: &Node) -> Self { fn new(weights: &Weights, root: &Node) -> Self {
Self { Self {
root_score: evaluate(root.matrix(), 0), weights: *weights,
root_score: features(root.matrix(), 0).evaluate(weights),
root_queue_len: root.queue().len(), root_queue_len: root.queue().len(),
} }
} }
fn evaluate(&self, mat: &Mat, queue: Queue<'_>) -> i32 { fn evaluate(&self, mat: &Mat, queue: Queue<'_>) -> i32 {
let pcnt = self.root_queue_len.saturating_sub(queue.len());
// FIXME: the old blockfish has two special edge cases for rating nodes that is // FIXME: the old blockfish has two special edge cases for rating nodes that is
// not done here. // not done here.
// //
@ -93,8 +93,11 @@ impl Evaluator {
// avoided by holding the last piece. i think this improves the performance only // 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. // 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 score = features(mat, pcnt).evaluate(&self.weights);
// larger (i.e., further below the root score) is better // larger (i.e., further below the root score) is better
self.root_score - evaluate(mat, pcnt) self.root_score - score
} }
} }

View File

@ -1,226 +1,64 @@
use mino::matrix::{COLUMNS, EMPTY_ROW, FULL_ROW};
use mino::Mat; use mino::Mat;
mod basic;
mod downstacking; mod downstacking;
pub use downstacking::mystery_mdse; #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Features(pub usize, pub [i32; 4]);
pub fn evaluate(mat: &Mat, pcnt: usize) -> i32 { /// Computes the feature vector for the given matrix. These are the underlying values
// TODO: public interface for weights etc. /// used to calculate the heuristic rating.
pub fn features(mat: &Mat, pcnt: usize) -> Features {
struct Weights { Features(
height: i32, pcnt,
i_deps: i32, [
mdse: i32, basic::max_height(mat),
pcnt: i32, basic::i_deps(mat),
} basic::row_trans(mat),
downstacking::mystery_mdse(mat),
const W: Weights = Weights { ],
height: 5, )
i_deps: 10,
mdse: 10,
pcnt: 10,
};
let mut rating = 0;
rating += max_height(mat) * W.height;
rating += i_deps(mat) * W.i_deps;
rating += mystery_mdse(mat) * W.mdse;
rating += (pcnt as i32) * W.pcnt;
rating
} }
pub fn max_height(matrix: &Mat) -> i32 { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
matrix.rows() as i32 pub struct Weights(pub [i32; 4]);
impl Weights {
/// Default weights (determined experimentally).
pub const DEFAULT: Self = Self([
512, // max_height
1024, // i_deps
0, // FIXME: row_trans
1024, // mdse
]);
/// Constant penalty given to the number of pieces placed. Each element of the weight
/// vector is effectively a ratio to this constant, i.e. if this were to be doubled,
/// then all of the weights should be doubled as well to produce an identical
/// heuristic.
pub const PER_PIECE: i32 = 1024;
} }
pub fn i_deps(matrix: &Mat) -> i32 { impl Default for Weights {
let mut depth = [0u8; COLUMNS as usize]; fn default() -> Self {
let mut count = 0; Self::DEFAULT
for y in 0..matrix.rows() { }
// 012345689xxxx }
// ↑
// _______xxx___ impl Features {
let mut mask = 0b111 << (COLUMNS - 2); /// Applies the given weights vector to the features. Returns the heuristic score.
// _______x.x___ #[inline]
let mut test = 0b101 << (COLUMNS - 2); pub fn evaluate(&self, weights: &Weights) -> i32 {
for x in (0..COLUMNS).rev() { let pcnt = self.0 as i32;
if matrix.test_row(y, mask, test) { let ft = &self.1;
depth[x as usize] += 1; let wt = &weights.0;
depth[x as usize] %= 4;
if depth[x as usize] == 3 { let mut score = Weights::PER_PIECE * pcnt;
count += 1; score += wt[0] * ft[0];
} score += wt[1] * ft[1];
} else { score += wt[2] * ft[2];
depth[x as usize] = 0; score += wt[3] * ft[3];
} // TODO(?) quadratic weights, e.g. score += wt[i] * f[j]^2
mask >>= 1; score
test >>= 1;
}
}
count
}
pub fn row_trans(matrix: &Mat) -> i32 {
let mut prev_row = FULL_ROW;
let mut count = 0;
for &curr_row in &matrix[..] {
count += (curr_row ^ prev_row).count_ones();
prev_row = curr_row;
}
count += (prev_row ^ EMPTY_ROW).count_ones();
count as i32
}
#[cfg(test)]
mod test {
use super::*;
use mino::mat;
#[test]
fn test_i_deps() {
assert_eq!(i_deps(Mat::EMPTY), 0);
assert_eq!(
i_deps(mat! {
"xxxxx...xx";
"xxxxx..xxx";
"xxxxx.xx.x";
"xxxxx.xxxx";
}),
0
);
assert_eq!(
i_deps(mat! {
"xxx.....xx";
"xxxx..xxxx";
"xxxxx.xxxx";
"x.xxx.xxxx";
}),
0
);
assert_eq!(
i_deps(mat! {
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
1
);
assert_eq!(
i_deps(mat! {
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
1
);
assert_eq!(
i_deps(mat! {
"xxxxxxxx.x";
"xxxxxxxx.x";
"xxxxxxxx.x";
"xxxxxxxx.x";
"x.xxxxxxxx";
"x.xxxxxxxx";
"x.xxxxxxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
3,
);
assert_eq!(
i_deps(mat! {
// 6 rows
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
1
);
assert_eq!(
i_deps(mat! {
// 7 rows
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
2
);
assert_eq!(
i_deps(mat! {
"x.x......x";
"x.xxx.xxxx";
"x.xxx.xxxx";
"x.xxx.xxxx";
}),
2
);
}
#[test]
fn test_row_trans() {
assert_eq!(row_trans(Mat::EMPTY), 10);
assert_eq!(
row_trans(mat! {
"x.........";
"xx........";
"xxx.......";
"xxxx......";
"xxxxx.....";
"xxxxxx....";
"xxxxxxx...";
"xxxxxxxx..";
"xxxxxxxxx.";
"xxxxxxxxxx";
}),
10,
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxxx";
}),
9 + 1,
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
9 + 1,
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxxx";
"xxxx.Xxxxx";
}),
9 + 2 + 1
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxx.";
"xx....xxxx";
}),
8 + 4 + 4
);
assert_eq!(
row_trans(mat! {
"xxxx..xxxx";
"xxxx...xxx";
"xxxxx.xxxx";
}),
8 + 1 + 2 + 1,
);
} }
} }

196
fish/src/eval/basic.rs Normal file
View File

@ -0,0 +1,196 @@
use mino::matrix::{Mat, COLUMNS, EMPTY_ROW, FULL_ROW};
pub fn max_height(matrix: &Mat) -> i32 {
matrix.rows() as i32
}
pub fn i_deps(matrix: &Mat) -> i32 {
let mut depth = [0u8; COLUMNS as usize];
let mut count = 0;
for y in 0..matrix.rows() {
// 012345689xxxx
// ↑
// _______xxx___
let mut mask = 0b111 << (COLUMNS - 2);
// _______x.x___
let mut test = 0b101 << (COLUMNS - 2);
for x in (0..COLUMNS).rev() {
if matrix.test_row(y, mask, test) {
depth[x as usize] += 1;
depth[x as usize] %= 4;
if depth[x as usize] == 3 {
count += 1;
}
} else {
depth[x as usize] = 0;
}
mask >>= 1;
test >>= 1;
}
}
count
}
pub fn row_trans(matrix: &Mat) -> i32 {
let mut prev_row = FULL_ROW;
let mut count = 0;
for &curr_row in &matrix[..] {
count += (curr_row ^ prev_row).count_ones();
prev_row = curr_row;
}
count += (prev_row ^ EMPTY_ROW).count_ones();
count as i32
}
#[cfg(test)]
mod test {
use super::*;
use mino::mat;
#[test]
fn test_i_deps() {
assert_eq!(i_deps(Mat::EMPTY), 0);
assert_eq!(
i_deps(mat! {
"xxxxx...xx";
"xxxxx..xxx";
"xxxxx.xx.x";
"xxxxx.xxxx";
}),
0
);
assert_eq!(
i_deps(mat! {
"xxx.....xx";
"xxxx..xxxx";
"xxxxx.xxxx";
"x.xxx.xxxx";
}),
0
);
assert_eq!(
i_deps(mat! {
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
1
);
assert_eq!(
i_deps(mat! {
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
1
);
assert_eq!(
i_deps(mat! {
"xxxxxxxx.x";
"xxxxxxxx.x";
"xxxxxxxx.x";
"xxxxxxxx.x";
"x.xxxxxxxx";
"x.xxxxxxxx";
"x.xxxxxxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
3,
);
assert_eq!(
i_deps(mat! {
// 6 rows
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
1
);
assert_eq!(
i_deps(mat! {
// 7 rows
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
2
);
assert_eq!(
i_deps(mat! {
"x.x......x";
"x.xxx.xxxx";
"x.xxx.xxxx";
"x.xxx.xxxx";
}),
2
);
}
#[test]
fn test_row_trans() {
assert_eq!(row_trans(Mat::EMPTY), 10);
assert_eq!(
row_trans(mat! {
"x.........";
"xx........";
"xxx.......";
"xxxx......";
"xxxxx.....";
"xxxxxx....";
"xxxxxxx...";
"xxxxxxxx..";
"xxxxxxxxx.";
"xxxxxxxxxx";
}),
10,
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxxx";
}),
9 + 1,
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
"xxxxx.xxxx";
}),
9 + 1,
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxxx";
"xxxx.Xxxxx";
}),
9 + 2 + 1
);
assert_eq!(
row_trans(mat! {
"xxxxx.xxx.";
"xx....xxxx";
}),
8 + 4 + 4
);
assert_eq!(
row_trans(mat! {
"xxxx..xxxx";
"xxxx...xxx";
"xxxxx.xxxx";
}),
8 + 1 + 2 + 1,
);
}
}

View File

@ -5,9 +5,9 @@ use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
use std::sync::{atomic, mpsc, Arc}; use std::sync::{atomic, mpsc, Arc};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tidepool::output::SummaryStats;
use fish::bot; use fish::{bot, eval};
use tidepool::output::SummaryStats;
use tidepool::{cli, config, output, sim}; use tidepool::{cli, config, output, sim};
fn main() -> Result<()> { fn main() -> Result<()> {
@ -338,6 +338,7 @@ fn run_simulation(
) { ) {
let mut moves = data_args.list_moves.then(Vec::new); let mut moves = data_args.list_moves.then(Vec::new);
let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64()); let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64());
let weights = eval::Weights::default();
let sim_opts = sim::Options { let sim_opts = sim::Options {
goal: config.game.goal, goal: config.game.goal,
@ -354,6 +355,7 @@ fn run_simulation(
} }
let mut bot = bot::Bot::new( let mut bot = bot::Bot::new(
&weights,
sim.matrix(), sim.matrix(),
mino::Queue::new(sim.queue().hold(), sim.queue().next()), mino::Queue::new(sim.queue().hold(), sim.queue().next()),
); );