split off logic
This commit is contained in:
358
src/logic/future_moves.rs
Normal file
358
src/logic/future_moves.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
use indicatif::{ProgressIterator, ProgressStyle};
|
||||
|
||||
use crate::{
|
||||
board::{Board, Winner},
|
||||
logic::r#move::Move,
|
||||
piece::Piece,
|
||||
};
|
||||
|
||||
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,
|
||||
|
||||
/// Target depth of children to generate
|
||||
max_depth: usize,
|
||||
|
||||
/// How many deep should the lazy children status expire?
|
||||
lazy_expire: usize,
|
||||
|
||||
/// Color w.r.t
|
||||
agent_color: Piece,
|
||||
}
|
||||
|
||||
impl FutureMoves {
|
||||
pub const fn new(agent_color: Piece, max_depth: usize, lazy_expire: usize) -> Self {
|
||||
Self {
|
||||
arena: Vec::new(),
|
||||
current_root: None,
|
||||
current_depth: 0,
|
||||
max_depth,
|
||||
agent_color,
|
||||
lazy_expire,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.arena.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Generate children for all children of `nodes`
|
||||
/// only `pub` for the sake of benchmarking
|
||||
pub fn extend_layers(&mut self) {
|
||||
let mut next_nodes: 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.lazy_children || got.children.is_empty()
|
||||
})
|
||||
.filter(|&idx| self.is_connected_to_root(idx)) // put here so this will not extend needlessly before prunes
|
||||
.collect();
|
||||
|
||||
for i in self.current_depth..=self.max_depth {
|
||||
next_nodes = next_nodes
|
||||
.into_iter()
|
||||
.progress_with_style(
|
||||
ProgressStyle::with_template(&format!(
|
||||
"Generating children (depth: {}/{}): ({{pos}}/{{len}}) {{per_sec}}",
|
||||
i, self.max_depth
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.flat_map(|node_idx| {
|
||||
self.generate_children(
|
||||
node_idx,
|
||||
if self.arena[node_idx].lazy_children {
|
||||
self.depth_of(node_idx) - 1
|
||||
} else {
|
||||
i
|
||||
} > self.lazy_expire,
|
||||
)
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
}
|
||||
self.current_depth = self.max_depth;
|
||||
}
|
||||
|
||||
/// 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`), returns an iterator it's children's indexes
|
||||
fn generate_children(
|
||||
&mut self,
|
||||
parent_idx: usize,
|
||||
lazy_children: bool,
|
||||
) -> Option<impl Iterator<Item = usize>> {
|
||||
let parent = &self.arena[parent_idx];
|
||||
|
||||
// early-exit if a winner for the parent already exists
|
||||
if parent.winner != Winner::None {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_color = !parent.color;
|
||||
let mut new: Vec<Move> =
|
||||
// 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`]
|
||||
Board::all_positions()
|
||||
.flat_map(|(i, j)| parent.board.what_if(i, j, new_color).map(move |x| (i, j, x)))
|
||||
.map(|(i, j, new_board)| Move {
|
||||
i,
|
||||
j,
|
||||
board: new_board,
|
||||
winner: new_board.game_winner(),
|
||||
parent: Some(parent_idx),
|
||||
children: Vec::new(),
|
||||
value: 0,
|
||||
color: new_color,
|
||||
lazy_children,
|
||||
}).collect();
|
||||
|
||||
// negative, because we want the max value to be at the first index
|
||||
new.sort_by_key(|x| -x.compute_self_value(self.agent_color));
|
||||
|
||||
// keep the TOP_K children `self.agent_color`-color moves
|
||||
const TOP_K_CHILDREN: usize = 1;
|
||||
|
||||
// we want to keep only the best move of the agent
|
||||
if lazy_children && new_color == self.agent_color && new.len() > TOP_K_CHILDREN {
|
||||
new.drain(TOP_K_CHILDREN..);
|
||||
}
|
||||
|
||||
let start_idx = self.arena.len();
|
||||
if parent.lazy_children && !lazy_children {
|
||||
self.arena[parent_idx].lazy_children = false;
|
||||
// this move's children are being regenerated after lazy child expiration, don't append first node
|
||||
if new.len() > TOP_K_CHILDREN {
|
||||
self.arena.extend(new.drain(TOP_K_CHILDREN..));
|
||||
} else {
|
||||
// nothing will be appended
|
||||
// even though it was sorted the first time around
|
||||
// there's still no more than one element (which is already in the arena)
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
self.arena.extend(new);
|
||||
}
|
||||
let new_indices = start_idx..self.arena.len();
|
||||
|
||||
self.arena[parent_idx].children.extend(new_indices.clone());
|
||||
|
||||
Some(new_indices)
|
||||
}
|
||||
|
||||
/// Given an index from `self.arena`, what depth is it at? 1-indexed (ROOT IS AT INDEX 1)
|
||||
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
|
||||
}
|
||||
|
||||
fn compute_values(&mut self, indexes: impl Iterator<Item = usize>) {
|
||||
// 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
|
||||
let mut by_depth: Vec<Vec<usize>> = (0..=self.max_depth + 2).map(|_| Vec::new()).collect();
|
||||
for idx in indexes {
|
||||
let depth = self.depth_of(idx);
|
||||
// -1 because `depth_of` is one-indexed
|
||||
by_depth[depth - 1].push(idx);
|
||||
}
|
||||
|
||||
for (depth, nodes) in by_depth.into_iter().enumerate().rev() {
|
||||
for idx in nodes {
|
||||
// TODO! impl dynamic sorting based on children's states, maybe it propegates
|
||||
// upwards using the `parent` field
|
||||
// SAFETY! the sort_by_key function should not modify anything
|
||||
unsafe { (*(self as *mut Self)).arena.get_unchecked_mut(idx) }
|
||||
.children
|
||||
// negative because we want the largest value in the first index
|
||||
.sort_by_key(|&x| -self.arena[x].value);
|
||||
|
||||
let node = &self.arena[idx];
|
||||
let self_value = node.compute_self_value(self.agent_color) / (depth + 1) as i64;
|
||||
|
||||
let children_value = node
|
||||
.children
|
||||
.iter()
|
||||
.rev() // rev then reverse so we get an index starting from the back
|
||||
.enumerate()
|
||||
// since children are sorted by value, we should weight the first one more
|
||||
.map(|(i, &child)| self.arena[child].value * (i as i64 + 1))
|
||||
.sum::<i64>()
|
||||
.checked_div(node.children.len() as i64)
|
||||
.unwrap_or(0);
|
||||
|
||||
self.arena[idx].value = self_value + children_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn best_move(&self) -> Option<(usize, usize)> {
|
||||
self.current_root
|
||||
.and_then(|x| {
|
||||
self.arena[x]
|
||||
.children
|
||||
.iter()
|
||||
.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].coords())
|
||||
}
|
||||
|
||||
/// Updates `FutureMoves` based on the current state of the board
|
||||
/// The board is supposed to be after the opposing move
|
||||
pub fn update(&mut self, board: &Board) {
|
||||
let curr_board = self
|
||||
.arena
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, m)| {
|
||||
&m.board == board && (m.parent == self.current_root) && self.current_root.is_some()
|
||||
})
|
||||
.map(|(idx, _)| idx);
|
||||
|
||||
if let Some(curr_board_idx) = curr_board {
|
||||
self.update_root_idx(curr_board_idx);
|
||||
} else {
|
||||
self.create_root_raw(*board);
|
||||
self.update_root_idx(0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_root_raw(&mut self, board: Board) {
|
||||
self.arena.clear();
|
||||
self.arena.push(Move {
|
||||
i: 0,
|
||||
j: 0,
|
||||
board,
|
||||
winner: board.game_winner(),
|
||||
parent: None,
|
||||
children: Vec::new(),
|
||||
value: 0,
|
||||
color: !self.agent_color,
|
||||
lazy_children: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Update the root based on the coordinate of the move
|
||||
/// Returns a boolean, `true` if the operation was successful, false if not
|
||||
#[must_use = "You must check if the root was properly set"]
|
||||
pub fn update_root_coord(&mut self, i: usize, j: usize) -> bool {
|
||||
self.arena
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, node)| {
|
||||
node.parent == self.current_root
|
||||
&& self.current_root.is_some()
|
||||
&& node.coords() == (i, j)
|
||||
})
|
||||
.map(|x| x.0)
|
||||
.inspect(|&root| self.update_root_idx(root))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn update_root_idx_raw(&mut self, idx: usize) {
|
||||
self.current_root = Some(idx);
|
||||
self.current_depth -= self.depth_of(idx) - 1;
|
||||
}
|
||||
|
||||
fn update_root_idx(&mut self, idx: usize) {
|
||||
self.update_root_idx_raw(idx);
|
||||
|
||||
self.refocus_tree();
|
||||
self.extend_layers();
|
||||
self.compute_values(0..self.arena.len());
|
||||
}
|
||||
|
||||
/// 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()];
|
||||
|
||||
self.arena = retain
|
||||
.into_iter()
|
||||
.enumerate() // old_idx
|
||||
.zip(self.arena.drain(..))
|
||||
.flat_map(|((old_idx, keep), node)| keep.then_some((old_idx, node))) // filter out unrelated nodes
|
||||
.enumerate() // new_idx
|
||||
.map(|(new_idx, (old_idx, mut node))| {
|
||||
index_map[old_idx] = Some(new_idx);
|
||||
|
||||
if let Some(parent) = node.parent.as_mut() {
|
||||
if let Some(new_parent) = index_map[*parent] {
|
||||
*parent = new_parent;
|
||||
} else {
|
||||
// make sure we don't have dangling parents
|
||||
node.parent = None;
|
||||
}
|
||||
}
|
||||
|
||||
node.children.retain_mut(|c| {
|
||||
if let Some(new_c) = index_map[*c] {
|
||||
*c = new_c;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
node
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.current_root = index_map[root];
|
||||
}
|
||||
}
|
||||
2
src/logic/mod.rs
Normal file
2
src/logic/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod future_moves;
|
||||
mod r#move;
|
||||
66
src/logic/move.rs
Normal file
66
src/logic/move.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::{
|
||||
board::{Board, Winner},
|
||||
piece::Piece,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Move {
|
||||
/// `i` position of move
|
||||
pub i: usize,
|
||||
|
||||
/// `j` position of move
|
||||
pub j: usize,
|
||||
|
||||
/// [`Board`] state after move is made
|
||||
pub board: Board,
|
||||
|
||||
/// Current winner of the match
|
||||
pub winner: Winner,
|
||||
|
||||
/// Index of this move's parent
|
||||
pub parent: Option<usize>,
|
||||
|
||||
/// Indices of this Move's Children
|
||||
pub children: Vec<usize>,
|
||||
|
||||
/// Value of this move
|
||||
pub value: i64,
|
||||
|
||||
pub color: Piece,
|
||||
|
||||
pub lazy_children: bool,
|
||||
}
|
||||
|
||||
impl Move {
|
||||
pub const fn coords(&self) -> (usize, usize) {
|
||||
(self.i, self.j)
|
||||
}
|
||||
|
||||
pub fn compute_self_value(&self, agent_color: Piece) -> i64 {
|
||||
if self.winner == Winner::Player(!agent_color) {
|
||||
// if this board results in the opponent winning, MAJORLY negatively weigh this move
|
||||
// NOTE! this branch isn't completely deleted because if so, the bot wouldn't make a move.
|
||||
// We shouldn't prune branches because we still need to always react to the opponent's moves
|
||||
return i64::MIN;
|
||||
} else if self.winner == Winner::Player(agent_color) {
|
||||
// results in a win for the agent
|
||||
return i64::MAX;
|
||||
} else if self.winner == Winner::Tie {
|
||||
// idk what a Tie should be valued?
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut self_value = self.board.net_score(agent_color) as i64;
|
||||
let corner_v_agent = Board::sides()
|
||||
.filter(|&(i, j)| self.board.get_piece(i, j, agent_color))
|
||||
.count() as i64;
|
||||
let corner_v_not_agent = Board::sides()
|
||||
.filter(|&(i, j)| self.board.get_piece(i, j, !agent_color))
|
||||
.count() as i64;
|
||||
|
||||
// make net-corner capture important
|
||||
self_value += (corner_v_agent - corner_v_not_agent) * 4;
|
||||
|
||||
self_value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user