860 lines
28 KiB
Rust
860 lines
28 KiB
Rust
use crate::{
|
|
logic::r#move::Move,
|
|
repr::{Board, CoordPair, Piece, Winner},
|
|
};
|
|
use allocative::Allocative;
|
|
use indicatif::{ProgressIterator, ProgressStyle};
|
|
use std::{collections::HashMap, hash::BuildHasherDefault, ops::ControlFlow};
|
|
|
|
#[derive(Allocative)]
|
|
pub struct FutureMoves {
|
|
/// Arena containing all [`Move`]
|
|
arena: Vec<Move>,
|
|
|
|
/// Index of the [`Move`] tree's root node
|
|
current_root: Option<usize>,
|
|
|
|
/// Current generated depth of the Arena
|
|
current_depth: usize,
|
|
|
|
/// Color w.r.t
|
|
agent_color: Piece,
|
|
|
|
config: FutureMoveConfig,
|
|
|
|
board: Board,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Allocative)]
|
|
|
|
pub struct FutureMoveConfig {
|
|
/// Max depth of that we should try and traverse
|
|
pub max_depth: usize,
|
|
|
|
/// the min depth an arena should fill for pruning to happen
|
|
pub min_arena_depth: usize,
|
|
|
|
/// when pruning, keep the top_k # of children
|
|
pub top_k_children: usize,
|
|
|
|
// the lower the value, the more conservative the pruning is, what level to stop pruning at?
|
|
// a lower value allows more possible paths
|
|
pub up_to_minus: usize,
|
|
|
|
/// Max size of the arena, will not generate more if
|
|
/// the arena is of that size or bigger
|
|
pub max_arena_size: usize,
|
|
|
|
pub do_prune: bool,
|
|
|
|
pub print: bool,
|
|
|
|
pub children_eval_method: ChildrenEvalMethod,
|
|
}
|
|
|
|
impl std::fmt::Display for FutureMoveConfig {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "D{} ", self.max_depth)?;
|
|
if self.do_prune {
|
|
write!(f, "MD{} ", self.min_arena_depth)?;
|
|
write!(f, "K{} ", self.top_k_children)?;
|
|
write!(f, "UM{} ", self.up_to_minus)?;
|
|
} else {
|
|
write!(f, "MD_ ")?;
|
|
write!(f, "K_ ")?;
|
|
write!(f, "UM_ ")?;
|
|
}
|
|
if self.max_arena_size == usize::MAX {
|
|
write!(f, "SMAX ")?;
|
|
} else {
|
|
write!(f, "S{} ", self.max_arena_size)?;
|
|
}
|
|
|
|
write!(f, "P{} ", self.do_prune)?;
|
|
write!(f, "C{:?}", self.children_eval_method)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Allocative)]
|
|
#[allow(dead_code)]
|
|
pub enum ChildrenEvalMethod {
|
|
Average,
|
|
/// AverageDivDepth gives the agent a sense of
|
|
/// time when it comes to how far away a potential win or gain
|
|
/// is. This performs much better in the Elo Arena than `Average`
|
|
AverageDivDepth,
|
|
}
|
|
|
|
impl FutureMoves {
|
|
pub const fn new(agent_color: Piece, config: FutureMoveConfig) -> Self {
|
|
Self {
|
|
arena: Vec::new(),
|
|
current_root: None,
|
|
current_depth: 0,
|
|
agent_color,
|
|
config,
|
|
board: Board::new(),
|
|
}
|
|
}
|
|
|
|
/// Return the length of the Arena
|
|
pub fn arena_len(&self) -> usize {
|
|
self.arena.len()
|
|
}
|
|
|
|
/// Returns indexes of leaf [`Move`]s, sorted by their depth (increasing order)
|
|
fn leaf_moves(&self) -> Vec<usize> {
|
|
let mut indexes: Vec<usize> = (0..self.arena.len())
|
|
// we want to select all nodes that don't have children, or are lazy (need to maybe be regenerated)
|
|
.filter(|&idx| {
|
|
let got = &self.arena[idx];
|
|
!got.is_trimmed && got.children.is_empty() && got.winner == Winner::None
|
|
})
|
|
.filter(|&idx| self.is_connected_to_root(idx))
|
|
.collect();
|
|
|
|
// we want to try and make the tree even in depth
|
|
// by first generating children for younger moves
|
|
indexes.sort_by_key(|&x| self.depth_of(x));
|
|
indexes
|
|
}
|
|
|
|
/// Find the current depth of the arena by
|
|
/// looking at leaf moves and finding the smallest value
|
|
fn determine_current_depth(&self) -> Option<usize> {
|
|
// leaf_moves is sorted from min to max depth
|
|
self.leaf_moves().first().map(|&i| self.depth_of(i))
|
|
}
|
|
|
|
/// Generate children for all children of `nodes`
|
|
/// only `pub` for the sake of benchmarking
|
|
pub fn extend_layers(&mut self) {
|
|
// recover from partial tree extention
|
|
if let Some(current_depth) = self.determine_current_depth() {
|
|
self.current_depth = current_depth;
|
|
}
|
|
|
|
for _ in self.current_depth..self.config.max_depth {
|
|
let pstyle_inner = if cfg!(test) || !self.config.print {
|
|
""
|
|
} else {
|
|
&format!(
|
|
"Generating children (depth: {}/{}): ({{pos}}/{{len}}) {{per_sec}}",
|
|
self.current_depth + 1,
|
|
self.config.max_depth
|
|
)
|
|
};
|
|
|
|
let cf = self
|
|
.leaf_moves()
|
|
.into_iter()
|
|
.filter(|&i| self.depth_of(i) == self.current_depth)
|
|
.collect::<Vec<usize>>()
|
|
.into_iter()
|
|
.progress_with_style(ProgressStyle::with_template(pstyle_inner).unwrap())
|
|
.try_for_each(|node_idx| {
|
|
self.generate_children(node_idx);
|
|
|
|
if self.arena_len() >= self.config.max_arena_size {
|
|
ControlFlow::Break(())
|
|
} else {
|
|
ControlFlow::Continue(())
|
|
}
|
|
});
|
|
|
|
self.prune_bad_children();
|
|
|
|
if cf.is_break() {
|
|
return;
|
|
}
|
|
self.current_depth += 1;
|
|
}
|
|
}
|
|
|
|
/// Determines if a [`Move`] at index `idx` is connected to `self.current_root`
|
|
/// Returns `false` if `self.current_root` is None
|
|
fn is_connected_to_root(&self, idx: usize) -> bool {
|
|
if let Some(root) = self.current_root {
|
|
let mut current = Some(idx);
|
|
while let Some(parent_idx) = current {
|
|
if parent_idx == root {
|
|
return true;
|
|
}
|
|
current = self.arena[parent_idx].parent;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Creates children for a parent (`parent_idx`)
|
|
/// Completely unchecked, the caller should be the one who tests to make sure child generation
|
|
/// hasn't already been tried on a parent
|
|
fn generate_children(&mut self, parent_idx: usize) {
|
|
let parent = &self.arena[parent_idx];
|
|
|
|
let new_color = !parent.color;
|
|
let parent_board = self
|
|
.get_board_from_idx(parent_idx)
|
|
.expect("unable to get board");
|
|
|
|
// use [`Board::all_positions`] here instead of [`Board::possible_moves`]
|
|
// because we use [`Board::what_if`] later and we want to reduce calls to [`Board::propegate_from_dry`]
|
|
let mut new: Vec<Move> = Board::all_positions()
|
|
.flat_map(|coord| {
|
|
parent_board
|
|
.what_if(coord, new_color)
|
|
.map(move |x| (coord, x))
|
|
})
|
|
.map(|(coord, new_board)| {
|
|
Move::new(Some(coord), new_board, new_color, self.agent_color)
|
|
})
|
|
.collect();
|
|
|
|
// if there are no moves to take, making
|
|
// no move is also an option!
|
|
if new.is_empty() {
|
|
new.push(Move::new(None, parent_board, new_color, self.agent_color));
|
|
}
|
|
|
|
let start_idx = self.arena.len();
|
|
self.arena.extend(new);
|
|
|
|
let new_indices = start_idx..self.arena.len();
|
|
|
|
for child_idx in new_indices {
|
|
self.set_parent_child(parent_idx, child_idx);
|
|
}
|
|
}
|
|
|
|
/// Given an index from `self.arena`, what depth is it at? 0-indexed
|
|
fn depth_of(&self, node_idx: usize) -> usize {
|
|
let mut depth = 0;
|
|
let mut current = Some(node_idx);
|
|
while let Some(parent_idx) = current {
|
|
depth += 1;
|
|
current = self.arena[parent_idx].parent;
|
|
}
|
|
|
|
depth - 1
|
|
}
|
|
|
|
//// PERF! pre-organize all indexes based on what depth they're at
|
|
/// previously, I did a lookup map based on if a node was visited, still resulted in a full
|
|
/// O(n) iteration each depth
|
|
fn by_depth(&self, indexes: impl Iterator<Item = usize>) -> Vec<(usize, Vec<usize>)> {
|
|
let mut by_depth: HashMap<
|
|
usize,
|
|
Vec<usize>,
|
|
BuildHasherDefault<nohash_hasher::NoHashHasher<usize>>,
|
|
> = HashMap::with_hasher(BuildHasherDefault::default());
|
|
|
|
for idx in indexes {
|
|
let depth = self.depth_of(idx);
|
|
if let Some(got) = by_depth.get_mut(&depth) {
|
|
got.push(idx);
|
|
} else {
|
|
by_depth.insert(depth, vec![idx]);
|
|
}
|
|
}
|
|
let mut by_depth_vec: Vec<(usize, Vec<usize>)> = by_depth.into_iter().collect();
|
|
by_depth_vec.sort_by_key(|x| x.0);
|
|
by_depth_vec
|
|
}
|
|
|
|
/// Compute `Move.value`, propegating upwards from the furthest out Moves
|
|
/// in the Arena.
|
|
fn compute_values(&mut self, indexes: impl Iterator<Item = usize>) {
|
|
let by_depth_vec = self.by_depth(indexes);
|
|
|
|
// reversed so we build up the value of the closest (in time) moves from the future
|
|
for (depth, nodes) in by_depth_vec.into_iter().rev() {
|
|
for idx in nodes {
|
|
let children_values = self.arena[idx]
|
|
.children
|
|
.iter()
|
|
.flat_map(|&child| self.arena[child].value)
|
|
.collect::<Vec<_>>();
|
|
|
|
let children_value = match self.config.children_eval_method {
|
|
ChildrenEvalMethod::Average => children_values
|
|
.into_iter()
|
|
.sum::<i32>()
|
|
.checked_div(self.arena[idx].children.len() as i32),
|
|
|
|
ChildrenEvalMethod::AverageDivDepth => children_values
|
|
.into_iter()
|
|
.sum::<i32>()
|
|
.checked_div(self.arena[idx].children.len() as i32)
|
|
.and_then(|x| x.checked_div(depth as i32)),
|
|
}
|
|
.unwrap_or(0);
|
|
|
|
// we use `depth` and divided `self_value` by it, idk if this is worth it
|
|
// we should really setup some sort of ELO rating for each commit, playing them against
|
|
// each other or something, could be cool to benchmark these more subjective things, not
|
|
// just performance (cycles/time wise)
|
|
self.arena[idx].value = Some(self.arena[idx].self_value as i32 + children_value);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn move_history(&self, idx: usize) -> Option<Vec<(Option<CoordPair>, Piece)>> {
|
|
if let Some(root) = self.current_root {
|
|
let mut hist = Vec::new();
|
|
|
|
let mut current = Some(idx);
|
|
while let Some(parent_idx) = current {
|
|
if parent_idx == root {
|
|
break;
|
|
}
|
|
|
|
let n = &self.arena[parent_idx];
|
|
hist.push((n.coord, n.color));
|
|
current = n.parent;
|
|
}
|
|
hist.reverse();
|
|
|
|
if current != self.current_root {
|
|
return None;
|
|
}
|
|
|
|
Some(hist)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_board_from_idx(&self, idx: usize) -> Option<Board> {
|
|
if let Some(hist) = self.move_history(idx) {
|
|
let mut board = self.board;
|
|
for (m, c) in hist {
|
|
if let Some(m) = m {
|
|
board.place(m, c).expect("move would not propegate");
|
|
}
|
|
}
|
|
Some(board)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Return the best move which is a child of `self.current_root`
|
|
pub fn best_move(&self) -> Option<Option<CoordPair>> {
|
|
self.current_root
|
|
.and_then(|x| {
|
|
self.arena[x]
|
|
.children
|
|
.iter()
|
|
// this would be considered `minimax`
|
|
.max_by_key(|&&idx| self.arena[idx].value)
|
|
})
|
|
.inspect(|&&x| {
|
|
assert_eq!(
|
|
self.arena[x].color, self.agent_color,
|
|
"selected move color should be the same as the color of the agent"
|
|
);
|
|
})
|
|
.map(|&x| self.arena[x].coord)
|
|
}
|
|
|
|
/// Updates `FutureMoves` based on the current state of the board
|
|
/// The board is supposed to be after the opposing move
|
|
/// Returns whether or not the arena was regenerated (bool)
|
|
pub fn update_from_board(&mut self, board: &Board) -> bool {
|
|
let curr_board = (0..self.arena.len())
|
|
// needs to be every other generation in order to
|
|
// match the agent_color usually root or great-grand child
|
|
.filter(|&idx| self.depth_of(idx) % 2 == 0)
|
|
.find(|&idx| {
|
|
self.arena[idx].color == !self.agent_color
|
|
&& self.get_board_from_idx(idx).as_ref() == Some(board)
|
|
});
|
|
|
|
if let Some(curr_board_idx) = curr_board {
|
|
self.root_from_child_idx_board(curr_board_idx, *board);
|
|
false
|
|
} else {
|
|
if self.config.print && !self.arena.is_empty() {
|
|
println!("regenerating arena from board");
|
|
}
|
|
self.rebuild_from_board(*board);
|
|
true
|
|
}
|
|
}
|
|
|
|
pub fn generate(&mut self) {
|
|
self.extend_layers();
|
|
self.compute_values(0..self.arena.len());
|
|
}
|
|
|
|
fn rebuild_from_board(&mut self, board: Board) {
|
|
self.arena.clear();
|
|
self.arena
|
|
.push(Move::new(None, board, !self.agent_color, self.agent_color));
|
|
self.current_root = Some(0);
|
|
self.current_depth = 0;
|
|
self.board = board;
|
|
}
|
|
|
|
fn root_from_child_idx_board(&mut self, idx: usize, board: Board) {
|
|
self.current_depth -= self.depth_of(idx);
|
|
self.current_root = Some(idx);
|
|
self.board = board;
|
|
self.refocus_tree();
|
|
}
|
|
|
|
pub fn set_parent_child(&mut self, parent: usize, child: usize) {
|
|
self.arena[parent].children.push(child);
|
|
self.arena[child].parent = Some(parent);
|
|
}
|
|
|
|
/// Checks the consistancy of the Arena (parents and children)
|
|
/// returns a vector of errors ([`String`])
|
|
#[allow(dead_code)]
|
|
pub fn check_arena(&self) -> Vec<String> {
|
|
let mut errors = vec![];
|
|
for idx in 0..self.arena.len() {
|
|
let m = &self.arena[idx];
|
|
if let Some(parent) = m.parent {
|
|
if !(0..self.arena.len()).contains(&parent) {
|
|
errors.push(format!("{}: parent is out of range ({})", idx, parent));
|
|
}
|
|
|
|
if !self.arena[parent].children.contains(&idx) {
|
|
errors.push(format!(
|
|
"{}: parent ({}) doesn't list {} as child",
|
|
idx, parent, idx
|
|
));
|
|
}
|
|
}
|
|
|
|
for &child_idx in &m.children {
|
|
if !(0..self.arena.len()).contains(&child_idx) {
|
|
errors.push(format!("{}: parent is out of range ({})", idx, child_idx));
|
|
}
|
|
|
|
if self.arena[child_idx].parent != Some(idx) {
|
|
errors.push(format!(
|
|
"{}: child ({}) does not list self as parent",
|
|
idx, child_idx
|
|
));
|
|
}
|
|
}
|
|
|
|
if !self.is_connected_to_root(idx) {
|
|
errors.push(format!("{}: not connected to root in any way", idx));
|
|
}
|
|
}
|
|
errors
|
|
}
|
|
|
|
fn prune_bad_children(&mut self) {
|
|
if self.current_depth < self.config.min_arena_depth || !self.config.do_prune {
|
|
return;
|
|
}
|
|
|
|
// values are needed in order to prune and see what's best
|
|
self.compute_values(0..self.arena_len());
|
|
|
|
for (depth, indexes) in self.by_depth(0..self.arena.len()) {
|
|
// TODO! maybe update by_depth every iteration or something?
|
|
if depth > self.current_depth.saturating_sub(self.config.up_to_minus) {
|
|
return;
|
|
}
|
|
|
|
// only prune moves of the agent
|
|
if indexes.first().map(|&i| self.arena[i].color) != Some(self.agent_color) {
|
|
continue;
|
|
}
|
|
|
|
for idx in indexes {
|
|
// any nodes that have no parent node and aren't
|
|
// the root node, is orphaned
|
|
if self.arena[idx].parent.is_none() && self.current_root != Some(idx) {
|
|
// propegate orphan-ness to the children
|
|
let children: Vec<usize> = self.arena[idx].children.drain(..).collect();
|
|
for idx in children {
|
|
self.arena[idx].parent = None;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let mut m = self.arena[idx].clone();
|
|
// don't attempt and trim moves that have already been trimmed
|
|
if m.is_trimmed {
|
|
continue;
|
|
}
|
|
|
|
m.is_trimmed = true;
|
|
m.sort_children(&self.arena);
|
|
// prune the selected non top_k children
|
|
if m.children.len() > self.config.top_k_children {
|
|
// remove parent-child relation
|
|
let drained = m.children.drain(self.config.top_k_children..);
|
|
|
|
for idx in drained {
|
|
// remove child-parent relation
|
|
self.arena[idx].parent = None;
|
|
}
|
|
}
|
|
self.arena[idx] = m;
|
|
}
|
|
}
|
|
|
|
// rebuild tree to exclude the things that were pruned
|
|
self.refocus_tree();
|
|
}
|
|
|
|
/// Rebuilds the Arena based on `self.current_root`, prunes unrelated nodes
|
|
fn refocus_tree(&mut self) {
|
|
let Some(root) = self.current_root else {
|
|
return;
|
|
};
|
|
|
|
// make sure `root` doesn't reference another node
|
|
self.arena[root].parent = None;
|
|
|
|
let mut retain = vec![false; self.arena.len()];
|
|
|
|
// stack is going to be AT MAXIMUM, the size of the array,
|
|
// so lets just pre-allocate it
|
|
let mut stack: Vec<usize> = Vec::with_capacity(self.arena.len());
|
|
stack.push(root);
|
|
|
|
// traverse children of the current root
|
|
while let Some(idx) = stack.pop() {
|
|
retain[idx] = true;
|
|
stack.extend(self.arena[idx].children.iter());
|
|
}
|
|
|
|
let mut index_map = vec![None; self.arena.len()];
|
|
|
|
let (indexes, moves): (Vec<(usize, usize)>, Vec<Move>) = retain
|
|
.into_iter()
|
|
.enumerate() // old_idx
|
|
.zip(self.arena.drain(..))
|
|
.filter(|&((_, keep), _)| keep) // filter out un-related nodes
|
|
.map(|((old_idx, _), node)| (old_idx, node))
|
|
.enumerate() // new_idx
|
|
.map(|(new_idx, (old_idx, node))| ((new_idx, old_idx), node))
|
|
.unzip();
|
|
|
|
for (new_idx, old_idx) in indexes {
|
|
index_map[old_idx] = Some(new_idx);
|
|
}
|
|
|
|
self.arena = moves
|
|
.into_iter()
|
|
.map(|mut node| {
|
|
node.parent = node
|
|
.parent
|
|
.and_then(|parent| index_map.get(parent))
|
|
.and_then(|&x| x);
|
|
|
|
for c in node.children.as_mut_slice() {
|
|
*c = index_map
|
|
.get(*c)
|
|
.and_then(|&x| x)
|
|
.expect("index_map should contain the child's index");
|
|
}
|
|
|
|
node
|
|
})
|
|
.collect();
|
|
|
|
self.current_root = index_map[root];
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const FUTURE_MOVES_CONFIG: FutureMoveConfig = FutureMoveConfig {
|
|
max_depth: 3, // we want great-grand children for traversing moves
|
|
min_arena_depth: 0,
|
|
top_k_children: 1,
|
|
up_to_minus: 0,
|
|
max_arena_size: 100,
|
|
do_prune: false,
|
|
print: false,
|
|
children_eval_method: ChildrenEvalMethod::AverageDivDepth,
|
|
};
|
|
|
|
#[test]
|
|
fn prune_tree_test() {
|
|
let mut futm = FutureMoves::new(Piece::Black, FUTURE_MOVES_CONFIG);
|
|
|
|
futm.update_from_board(&Board::new());
|
|
|
|
// child 1
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
futm.set_parent_child(0, 1);
|
|
|
|
// dummy (2)
|
|
futm.arena.push(Move::new(
|
|
Some((9, 9).into()),
|
|
Board::new(),
|
|
Piece::White,
|
|
Piece::Black,
|
|
));
|
|
|
|
// 3
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
futm.set_parent_child(0, 3);
|
|
|
|
// 4
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
futm.set_parent_child(0, 4);
|
|
|
|
assert_eq!(futm.arena_len(), 5);
|
|
|
|
futm.refocus_tree();
|
|
assert_eq!(futm.arena_len(), 4);
|
|
assert_eq!(futm.arena[0].children.len(), 3);
|
|
|
|
assert_ne!(
|
|
futm.arena[2].coord,
|
|
Some((9, 9).into()),
|
|
"dummy value still exists"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn expand_layer_test() {
|
|
let mut futm = FutureMoves::new(Piece::Black, FUTURE_MOVES_CONFIG);
|
|
futm.config.max_depth = 1;
|
|
|
|
futm.update_from_board(&Board::new().starting_pos());
|
|
|
|
futm.extend_layers();
|
|
assert_eq!(futm.arena_len(), 5);
|
|
|
|
// move to a child
|
|
futm.root_from_child_idx_board(
|
|
1,
|
|
futm.get_board_from_idx(1)
|
|
.expect("unable to get board from child"),
|
|
);
|
|
futm.refocus_tree();
|
|
assert_eq!(futm.arena_len(), 1);
|
|
|
|
// make sure current_root is properly updated
|
|
assert_eq!(futm.current_root, Some(0));
|
|
|
|
futm.extend_layers();
|
|
assert!(
|
|
futm.arena_len() > 1,
|
|
"extend_layer didn't grow arena after refocus"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn depth_of_test() {
|
|
let mut futm = FutureMoves::new(Piece::Black, FUTURE_MOVES_CONFIG);
|
|
|
|
futm.update_from_board(&Board::new());
|
|
|
|
// child 1
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
futm.set_parent_child(0, 1);
|
|
|
|
// dummy
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
|
|
futm.set_parent_child(1, 3);
|
|
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
|
|
futm.set_parent_child(0, 4);
|
|
|
|
assert_eq!(futm.depth_of(3), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn by_depth_test() {
|
|
let mut futm = FutureMoves::new(Piece::Black, FUTURE_MOVES_CONFIG);
|
|
|
|
futm.update_from_board(&Board::new());
|
|
|
|
// child 1
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
futm.set_parent_child(0, 1);
|
|
|
|
// dummy
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
|
|
futm.arena
|
|
.push(Move::new(None, Board::new(), Piece::White, Piece::Black));
|
|
futm.set_parent_child(1, 3);
|
|
|
|
assert_eq!(
|
|
futm.by_depth(0..futm.arena.len()),
|
|
vec![(0, vec![0, 2]), (1, vec![1]), (2, vec![3])]
|
|
);
|
|
}
|
|
|
|
/// tests whether or not FutureMoves can recover from multiple skips and then manually regenerating the arena
|
|
#[test]
|
|
fn skip_move_recovery() {
|
|
let mut futm = FutureMoves::new(Piece::Black, FUTURE_MOVES_CONFIG);
|
|
let mut board = Board::new().starting_pos();
|
|
|
|
// replay of a test I did
|
|
// TODO! make this as small of a test as possible
|
|
let moves = vec![
|
|
(Some((5, 4)), Piece::Black),
|
|
(Some((5, 5)), Piece::White),
|
|
(Some((5, 6)), Piece::Black),
|
|
(Some((6, 4)), Piece::White),
|
|
(Some((7, 3)), Piece::Black),
|
|
(Some((7, 4)), Piece::White),
|
|
(Some((7, 5)), Piece::Black),
|
|
(Some((2, 4)), Piece::White),
|
|
(Some((1, 4)), Piece::Black),
|
|
(Some((1, 5)), Piece::White),
|
|
(Some((1, 6)), Piece::Black),
|
|
(Some((0, 6)), Piece::White),
|
|
(Some((3, 2)), Piece::Black),
|
|
(Some((1, 7)), Piece::White),
|
|
(None, Piece::Black), // black skips a move
|
|
(Some((0, 4)), Piece::White),
|
|
(None, Piece::Black), // black skips a move
|
|
(Some((4, 2)), Piece::White),
|
|
];
|
|
|
|
for (i, (coords, color)) in moves.into_iter().enumerate() {
|
|
if color == futm.agent_color {
|
|
// my turn
|
|
|
|
// seperate variable because we want it to always be executed
|
|
let update_result = futm.update_from_board(&board);
|
|
futm.generate();
|
|
|
|
// make sure that the arena should only be
|
|
// regenerated on the first move
|
|
if i > 0 && update_result {
|
|
panic!("board regenerated on move #{}", i);
|
|
}
|
|
|
|
let best_move = futm.best_move();
|
|
assert!(
|
|
best_move.is_some(),
|
|
"best_move (#{}) resulted in ABSOLUTE None: {:?}",
|
|
i,
|
|
best_move
|
|
);
|
|
|
|
if coords.is_none() {
|
|
assert_eq!(best_move, Some(None));
|
|
} else {
|
|
assert_ne!(best_move, Some(None));
|
|
}
|
|
}
|
|
|
|
if let Some(coord) = coords {
|
|
board.place(coord.into(), color).unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn derive_board() {
|
|
let mut futm = FutureMoves::new(Piece::White, FUTURE_MOVES_CONFIG);
|
|
|
|
let mut b = Board::new().starting_pos();
|
|
futm.update_from_board(&Board::new().starting_pos());
|
|
|
|
b.place((4, 2).into(), Piece::White).unwrap();
|
|
|
|
futm.arena.push(Move::new(
|
|
Some((4, 2).into()),
|
|
b,
|
|
Piece::White,
|
|
Piece::White,
|
|
));
|
|
|
|
futm.set_parent_child(0, 1);
|
|
|
|
assert_eq!(
|
|
futm.move_history(1),
|
|
Some(vec![(Some((4, 2).into()), Piece::White)]),
|
|
);
|
|
assert_eq!(futm.get_board_from_idx(1), Some(b));
|
|
|
|
b.place((5, 4).into(), Piece::Black).unwrap();
|
|
|
|
futm.arena.push(Move::new(
|
|
Some((5, 4).into()),
|
|
b,
|
|
Piece::Black,
|
|
Piece::White,
|
|
));
|
|
|
|
futm.set_parent_child(1, 2);
|
|
assert_eq!(
|
|
futm.move_history(2),
|
|
Some(vec![
|
|
(Some((4, 2).into()), Piece::White),
|
|
(Some((5, 4).into()), Piece::Black)
|
|
]),
|
|
);
|
|
assert_eq!(futm.get_board_from_idx(2), Some(b));
|
|
}
|
|
|
|
// I can't actually reproduce the issue I got, this is my best attempt
|
|
// Can't get it to fail!
|
|
#[test]
|
|
fn corr_moves() {
|
|
let mut board = Board::new();
|
|
board.place_unchecked((5, 4).into(), Piece::Black);
|
|
board.place_unchecked((4, 4).into(), Piece::Black);
|
|
board.place_unchecked((4, 3).into(), Piece::Black);
|
|
|
|
board.place_unchecked((3, 5).into(), Piece::White);
|
|
board.place_unchecked((3, 4).into(), Piece::Black);
|
|
board.place_unchecked((3, 3).into(), Piece::White);
|
|
|
|
board.place_unchecked((2, 4).into(), Piece::Black);
|
|
|
|
let move_log = vec![
|
|
(Some((4, 6)), Piece::Black),
|
|
(Some((5, 5)), Piece::White),
|
|
// (Some((3, 1)), Piece::Black), // invalid move
|
|
];
|
|
for (m, c) in move_log {
|
|
if let Some(m) = m {
|
|
board.place(m.into(), c).expect("invalid move");
|
|
}
|
|
}
|
|
|
|
let mut futm = FutureMoves::new(Piece::White, FUTURE_MOVES_CONFIG);
|
|
futm.update_from_board(&board);
|
|
futm.generate();
|
|
|
|
let best_move = futm.best_move();
|
|
|
|
if let Some(best_move) = best_move {
|
|
assert!(
|
|
board
|
|
.possible_moves(futm.agent_color)
|
|
.any(|x| Some(&x) == best_move.as_ref()),
|
|
"futm played an invalid move"
|
|
);
|
|
}
|
|
}
|
|
}
|