use indicatif::{ProgressIterator, ProgressStyle}; use crate::{ board::{Board, Winner}, piece::Piece, }; #[derive(Clone, Debug)] pub struct Move { /// `i` position of move i: usize, /// `j` position of move j: usize, /// [`Board`] state after move is made board: Board, /// Current winner of the match winner: Winner, /// Index of this move's parent parent: Option, /// Indices of this Move's Children children: Vec, /// Value of this move value: i64, color: Piece, lazy_children: bool, } impl Move { pub const fn coords(&self) -> (usize, usize) { (self.i, self.j) } 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 } } pub struct FutureMoves { /// Arena containing all [`Move`] arena: Vec, /// Index of the [`Move`] tree's root node current_root: Option, /// 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 = (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> { 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 = // 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) { // 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> = (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 { 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::() .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 = 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]; } }