From 19039f550b008086441684f0c0f9438cf4b854ac Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Sun, 9 Feb 2025 00:19:04 -0500 Subject: [PATCH] initial complexagent rewrite --- Cargo.lock | 52 ++++++++++ Cargo.toml | 1 + src/complexagent.rs | 227 ++++++++++++++++++++------------------------ 3 files changed, 154 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4249f1..698d7af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,37 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "getrandom" version = "0.3.1" @@ -123,6 +154,7 @@ version = "0.1.0" dependencies = [ "num", "rand", + "rayon", ] [[package]] @@ -183,6 +215,26 @@ dependencies = [ "zerocopy 0.8.16", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "syn" version = "2.0.98" diff --git a/Cargo.toml b/Cargo.toml index 47830a2..187fb90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] num = "0.4" rand = "0.9.0" +rayon = "1.10.0" diff --git a/src/complexagent.rs b/src/complexagent.rs index bd9a75e..0b99cff 100644 --- a/src/complexagent.rs +++ b/src/complexagent.rs @@ -1,8 +1,8 @@ use crate::{agent::Agent, board::Board, piece::Piece}; +use rayon::prelude::*; pub struct ComplexAgent { color: Piece, - curr_move: Option, } #[derive(Clone)] @@ -12,28 +12,13 @@ struct Move { captured: usize, color: Piece, board: Board, - next_move: Vec, winner: Option, + parent_index: Option, + value: i64, } impl Move { - fn populate_next_moves(&mut self, i: usize) { - if i == 0 { - return; - } - - // only compute next move if this move doesn't end the game - if self.winner.is_none() { - if self.next_move.is_empty() { - self.next_move = problem_space(&self.board, !self.color).collect(); - } - self.next_move - .iter_mut() - .for_each(|x| x.populate_next_moves(i - 1)); - } - } - - fn value(&self, agent_color: Piece, depth: usize) -> i64 { + fn compute_self_value(&self, agent_color: Piece, depth: usize) -> i64 { let mut self_value = self.captured as i64; if self.winner == Some(!agent_color) { @@ -42,118 +27,100 @@ impl 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 == Some(agent_color) { - // this move results in a + // results in a win for the agent self_value = i64::MAX; } else if agent_color != self.color { self_value = -self_value; } - // Reduce value of capture based on depth of prediction - self_value /= depth as i64; - - let avg_next_move_value = self - .next_move - .iter() - .map(|x| x.value(agent_color, depth + 1)) - .sum::() - .checked_div(self.next_move.len() as i64) - .unwrap_or(0); - - self_value + avg_next_move_value - } - - /// Returns the # of moves in the entire tree - pub fn len(&self) -> u32 { - self.next_move.len() as u32 + self.next_move.iter().map(Move::len).sum::() - } - - /// Returns a tuple containing the `i` and `j` fields - pub const fn coords(&self) -> (usize, usize) { - (self.i, self.j) - } - - /// Cursed function to create a dummy move type from a color and board - /// Used to bootstrap [`ComplexAgent`]'s future moves - pub fn bootstrap(color: Piece, board: &Board) -> Self { - Move { - i: 0, - j: 0, - captured: 0, - color, - board: *board, - next_move: vec![Move { - i: 0, - j: 0, - captured: 0, - color: !color, - board: *board, - next_move: problem_space(board, color).collect(), - winner: board.game_winner(!color), - }], - winner: board.game_winner(color), - } + self_value / depth as i64 } } -/// Take a [`Board`] and a [`Piece`] color and give all possible moves for that color on the board -/// Returns a Boxed iterator to be used in other logic -fn problem_space(board: &Board, color: Piece) -> Box + '_> { - Box::new( - board - .possible_moves(color) - .flat_map(move |(i, j)| board.what_if(i, j, color).map(|x| (i, j, x))) - .map(move |(i, j, (board, captured))| Move { - i, - j, - captured, - color, - board, - next_move: Vec::new(), - winner: board.game_winner(color), - }), - ) +impl ComplexAgent { + #[allow(dead_code)] + pub const fn new(color: Piece) -> Self { + Self { color } + } + + fn generate_layers(&self, board: &Board, depth: usize) -> Vec> { + let mut layers = Vec::with_capacity(depth); + let initial_moves: Vec = problem_space(board, self.color) + .map(|mut m| { + m.parent_index = None; + m.value = 0; + m + }) + .collect(); + layers.push(initial_moves); + + for current_depth in 0..depth { + let current_layer = &layers[current_depth]; + if current_layer.is_empty() { + break; + } + + let next_moves: Vec = current_layer + .par_iter() + .enumerate() + .flat_map(|(parent_idx, parent_move)| { + let opponent_color = !parent_move.color; + problem_space(&parent_move.board, opponent_color) + .map(|mut m| { + m.parent_index = Some(parent_idx); + m.value = 0; + m + }) + .collect::>() + }) + .collect(); + + layers.push(next_moves); + } + + self.compute_values(&mut layers); + layers + } + + fn compute_values(&self, layers: &mut [Vec]) { + let agent_color = self.color; + let layers_len = layers.len(); + for depth in (0..layers.len()).rev() { + let layer_depth_1: Vec = if depth + 1 < layers_len { + layers[depth + 1].clone() + } else { + Vec::new() + }; + + let current_layer = &mut layers[depth]; + + current_layer.par_iter_mut().for_each(|mv| { + let self_value = mv.compute_self_value(agent_color, depth + 1); + let children_value = if depth + 1 < layers_len { + layer_depth_1 + .iter() + .filter(|child| child.parent_index == Some(mv.parent_index.unwrap_or(0))) + .map(|child| child.value) + .sum::() + / layer_depth_1.len() as i64 + } else { + 0 + }; + mv.value = self_value + children_value; + }); + } + } } impl Agent for ComplexAgent { fn next_move(&mut self, board: &Board) -> Option<(usize, usize)> { const LOOPS: usize = 5; - - let curr_move: Move = self - .curr_move - .take() - .unwrap_or_else(|| Move::bootstrap(self.color, board)); - - // determine the move the other player made via the board state - let mut other_player_move = curr_move - .next_move - .into_iter() - .filter(|x| &x.board == board) - .last() - // handle invalid other player moves - .unwrap_or_else(|| { - println!("invalid board, rebuilding move tree..."); - - // rebuild move tree - // need to start with a !self.color move, so unwrap the first level - Move::bootstrap(self.color, board).next_move.remove(0) - }); - - other_player_move.populate_next_moves(LOOPS); - println!( - "(depth: {}) possible board states: {}", - LOOPS, - other_player_move.len() - ); - - // Take the best move and move it, don't clone the reference - self.curr_move = other_player_move - .next_move - .into_iter() - .max_by_key(|m| m.value(self.color, 1)); - - assert!(self.curr_move.is_some(), "ComplexAgent didn't make a move"); - - self.curr_move.as_ref().map(Move::coords) + let layers = self.generate_layers(board, LOOPS); + println!("len: {}", layers.iter().map(Vec::len).sum::()); + layers[0] + .par_iter() + .max_by_key(|m| m.value) + .map(|m| (m.i, m.j)) } fn name(&self) -> &'static str { @@ -165,12 +132,20 @@ impl Agent for ComplexAgent { } } -impl ComplexAgent { - #[allow(dead_code)] - pub const fn new(color: Piece) -> Self { - Self { - color, - curr_move: None, - } - } +fn problem_space(board: &Board, color: Piece) -> Box + '_> { + Box::new( + board + .possible_moves(color) + .flat_map(move |(i, j)| board.what_if(i, j, color).map(|x| (i, j, x))) + .map(move |(i, j, (board, captured))| Move { + i, + j, + captured, + color, + board, + winner: board.game_winner(color), + parent_index: None, + value: 0, + }), + ) }