use crate::{ agent::Agent, board::{Board, Winner}, piece::Piece, }; use indicatif::{ProgressBar, ProgressIterator, ProgressStyle}; #[derive(Clone, Debug)] 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, } impl Move { fn compute_self_value(&self, agent_color: Piece, depth: i64) -> i64 { let mut self_value = self.value; 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 self_value = i64::MIN; } else if self.winner == Winner::Player(agent_color) { // results in a win for the agent self_value = i64::MAX; } // TODO! handle ties... what should they be valued as? maybe `i64::MAX / 2`? self_value / depth } } struct FutureMoves { arena: Vec, current_root: Option, current_depth: isize, max_depth: usize, /// Color w.r.t agent_color: Piece, } impl FutureMoves { pub const fn new(agent_color: Piece, max_depth: usize) -> Self { Self { arena: Vec::new(), current_root: None, current_depth: 0, max_depth, agent_color, } } /// Generate children for all children of `nodes` fn extend_layers(&mut self) { let mut next_nodes: Vec = (0..self.arena.len()) .filter(|&idx| self.arena[idx].children.is_empty()) .collect(); for _ in self.current_depth..=(self.max_depth as isize) { let prog_len = next_nodes.len(); next_nodes = next_nodes .into_iter() .progress_with( ProgressBar::new(prog_len as u64).with_style( ProgressStyle::with_template( "Generating children: ({pos}/{len}) {per_sec}", ) .unwrap(), ), ) .flat_map(|node_idx| { self.generate_children( Some(node_idx), &self.arena[node_idx].board.clone(), !self.arena[node_idx].color, ) }) .collect(); } self.current_depth = self.max_depth as isize; } /// Creates children for a parent (`parent`), returns an iterator it's children's indexes fn generate_children( &mut self, parent: Option, board: &Board, color: Piece, ) -> impl Iterator { let start_idx = self.arena.len(); 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)| board.what_if(i, j, color).map(|x| (i, j, x))) .map(|(i, j, new_board)| Move { i, j, board: new_board, winner: new_board.game_winner(color), parent, children: Vec::new(), value: new_board.count(self.agent_color) as i64 - new_board.count(!self.agent_color) as i64, color: parent.map(|idx| !self.arena[idx].color).unwrap_or(self.agent_color) }).collect(); // we want to keep only the best move of the agent if color == self.agent_color { if new.len() > 1 { // 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, 1)); new.drain(1..); } } self.arena.extend(new); let new_indices = start_idx..self.arena.len(); if let Some(parent_idx) = parent { self.arena[parent_idx].children.extend(new_indices.clone()); } 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) { // PERF! doing this filtering here previously visited moves reduces time // spent in `depth_of` from 27.79% -> 2.9% let mut visited = vec![false; self.arena.len()]; for depth in (0..=self.current_depth).rev() { for idx in 0..self.arena.len() { if visited[idx] { continue; } else { visited[idx] = true; } if self.depth_of(idx) != depth as usize { continue; } let self_value = self.arena[idx] .compute_self_value(self.agent_color, (self.current_depth - depth + 1) as i64); let children_value = self.arena[idx] .children .iter() .map(|&child| self.arena[child].value) .sum::() .checked_div(self.arena[idx].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].i, self.arena[x].j)) } /// 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.current_root = Some(curr_board_idx); self.update_root_idx(curr_board_idx); } else { println!("Generating root of FutureMoves"); self.arena.clear(); self.arena.push(Move { i: 0, j: 0, board: *board, winner: Winner::None, parent: None, children: Vec::new(), value: 0, color: !self.agent_color, }); self.update_root_idx(0); } } pub fn update_root_coord(&mut self, i: usize, j: usize) -> bool { self.arena .iter() .enumerate() .find_map(|(idx, node)| { (node.parent == self.current_root && node.i == i && node.j == j).then_some(idx) }) .inspect(|&root| self.update_root_idx(root)) .is_some() } fn update_root_idx(&mut self, idx: usize) { self.current_root = Some(idx); self.current_depth -= self.depth_of(idx) as isize - 1; self.prune_unrelated(); self.extend_layers(); self.compute_values(); } fn prune_unrelated(&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); node.parent = node.parent.and_then(|p| index_map[p]); 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]; } } pub struct ComplexAgent { color: Piece, future_moves: FutureMoves, } impl ComplexAgent { pub const fn new(color: Piece) -> Self { const MAX_DEPTH: usize = 17; Self { color, future_moves: FutureMoves::new(color, MAX_DEPTH), } } } impl Agent for ComplexAgent { fn next_move(&mut self, board: &Board) -> Option<(usize, usize)> { self.future_moves.update(board); println!("# of moves stored: {}", self.future_moves.arena.len()); self.future_moves.best_move().inspect(|&(i, j)| { self.future_moves.update_root_coord(i, j); }) } fn name(&self) -> &'static str { "Complex Agent" } fn color(&self) -> Piece { self.color } }