From a95b9e08c1969f012211b7538dba78b20fd8e8c2 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Tue, 25 Feb 2025 15:03:25 -0500 Subject: [PATCH] small rewrite and addition of by_depth + by_depth_test --- Cargo.lock | 7 ++ Cargo.toml | 1 + src/complexagent.rs | 4 +- src/logic/future_moves.rs | 161 +++++++++++++++++++++++++------------- src/logic/move.rs | 22 +++--- 5 files changed, 129 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99e89c0..9ad2c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,6 +334,12 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "num" version = "0.4.3" @@ -436,6 +442,7 @@ dependencies = [ "either", "indicatif", "lazy_static", + "nohash-hasher", "num", "rand", "static_assertions", diff --git a/Cargo.toml b/Cargo.toml index fc83ed9..ce0ed3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ const_fn = "0.4" either = "1.13" indicatif = "0.17" lazy_static = "1.5" +nohash-hasher = "0.2.0" num = "0.4" rand = "0.9" static_assertions = "1.1" diff --git a/src/complexagent.rs b/src/complexagent.rs index b636e19..5719bfe 100644 --- a/src/complexagent.rs +++ b/src/complexagent.rs @@ -12,8 +12,8 @@ pub struct ComplexAgent { #[allow(dead_code)] impl ComplexAgent { pub const fn new(color: Piece) -> Self { - const MAX_DEPTH: usize = 8; - const NON_LAZY_DEPTH: usize = 8; + const MAX_DEPTH: usize = 5; + const NON_LAZY_DEPTH: usize = 3; Self { color, future_moves: FutureMoves::new(color, MAX_DEPTH, NON_LAZY_DEPTH), diff --git a/src/logic/future_moves.rs b/src/logic/future_moves.rs index 8005316..697f95a 100644 --- a/src/logic/future_moves.rs +++ b/src/logic/future_moves.rs @@ -3,7 +3,7 @@ use crate::{ repr::{Board, Piece, Winner}, }; use indicatif::{ProgressIterator, ProgressStyle}; -use std::collections::HashMap; +use std::{collections::HashMap, hash::BuildHasherDefault}; pub struct FutureMoves { /// Arena containing all [`Move`] @@ -49,7 +49,7 @@ impl FutureMoves { // 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() + got.is_lazy || got.children.is_empty() }) .filter(|&idx| self.is_connected_to_root(idx)) // put here so this will not extend needlessly before prunes .collect(); @@ -65,14 +65,14 @@ impl FutureMoves { .unwrap(), ) .flat_map(|node_idx| { - self.generate_children( - node_idx, - if self.arena[node_idx].lazy_children { - self.depth_of(node_idx) - } else { - i - } > self.lazy_expire, - ) + if (self.arena[node_idx].is_lazy + && self.depth_of(node_idx) + 1 > self.lazy_expire) + || !self.arena[node_idx].is_lazy + { + self.generate_children(node_idx, i > self.lazy_expire) + } else { + None + } }) .flatten() .collect(); @@ -126,7 +126,6 @@ impl FutureMoves { j, new_board, new_color, - lazy_children, self.agent_color, Some(parent_idx), ) @@ -134,7 +133,7 @@ impl FutureMoves { .collect(); // keep the TOP_K children of their magnitude - const TOP_K_CHILDREN: usize = 10; + const TOP_K_CHILDREN: usize = 3; let start_idx = self.arena.len(); self.arena.extend(new); @@ -142,13 +141,14 @@ impl FutureMoves { let new_indices = start_idx..self.arena.len(); self.arena[parent_idx].children.extend(new_indices.clone()); - let mut parent_copy = self.arena[parent_idx].clone(); - parent_copy.sort_children(self.arena.as_mut_slice()); - self.arena[parent_idx] = parent_copy; if lazy_children && new_indices.clone().count() > TOP_K_CHILDREN { + let mut parent_copy = self.arena[parent_idx].clone(); + parent_copy.sort_children(self.arena.as_mut_slice()); + self.arena[parent_idx] = parent_copy; + for i in new_indices.clone().skip(TOP_K_CHILDREN) { - self.arena[i].lazy_children = true; + self.arena[i].is_lazy = true; } } @@ -167,13 +167,12 @@ impl FutureMoves { depth - 1 } - /// Compute `Move.value`, propegating upwards from the furthest out Moves - /// in the Arena. - 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: HashMap> = HashMap::new(); + fn by_depth(&self, indexes: impl Iterator) -> Vec<(usize, Vec)> { + let mut by_depth: HashMap< + usize, + Vec, + BuildHasherDefault>, + > = HashMap::with_hasher(BuildHasherDefault::default()); for idx in indexes { let depth = self.depth_of(idx); if let Some(got) = by_depth.get_mut(&depth) { @@ -182,36 +181,42 @@ impl FutureMoves { by_depth.insert(depth, vec![idx]); } } - let mut by_depth_vec: Vec<(usize, Vec)> = by_depth.into_iter().collect(); by_depth_vec.sort_by_key(|x| x.0); + by_depth_vec + } - for (depth, nodes) in by_depth_vec { + /// Compute `Move.value`, propegating upwards from the furthest out Moves + /// in the Arena. + 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 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 { // 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 { &mut (*(self as *mut Self)) }.arena[idx] - .children - // negative because we want the largest value in the first index - // abs so we get the most extreme solutions - // but base on `.value` for recursive behavior - .sort_by_key(|&x| -self.arena[x].value.abs()); + + // let mut parent_copy = self.arena[idx].clone(); + // parent_copy.sort_children(self.arena.as_mut_slice()); + // self.arena[idx] = parent_copy; let children_value = self.arena[idx] .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 + 1) as i128)) + .map(|&child| self.arena[child].value.expect("child has no value??")) .sum::(); - // previously we used `depth` and divided `self_value` by it, idk if this is worth it + // 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 - self.arena[idx].value = self.arena[idx].self_value as i128 + children_value; + // just performance (cycles/time wise) + self.arena[idx].value = Some( + (self.arena[idx].self_value as i128 + children_value) / (depth + 1) as i128, + ); } } } @@ -260,7 +265,6 @@ impl FutureMoves { 0, // dummy board, !self.agent_color, - false, self.agent_color, None, )); @@ -347,7 +351,7 @@ impl FutureMoves { let mut children = self.arena[root].children.clone(); - children.sort_by_key(|&i| -self.arena[i].value); + children.sort_by_key(|&i| -self.arena[i].value.expect("child has no value")); let start_len = ((children.len()) as f32 * (1.0 - BOTTOM_PERC)) as usize; let drained = children.drain(start_len..); println!("{}", drained.len()); @@ -439,10 +443,10 @@ mod tests { winner: Winner::None, parent: None, children: vec![1, 3, 4], - value: 0, + value: None, self_value: 0, color: Piece::Black, - lazy_children: false, + is_lazy: false, }); futm.update_root_idx_raw(0); @@ -453,7 +457,6 @@ mod tests { 0, Board::new(), Piece::White, - false, Piece::Black, Some(0), )); @@ -464,7 +467,6 @@ mod tests { 1234, Board::new(), Piece::White, - false, Piece::Black, None, )); @@ -474,7 +476,6 @@ mod tests { 0, Board::new(), Piece::White, - false, Piece::Black, Some(0), )); @@ -484,7 +485,6 @@ mod tests { 0, Board::new(), Piece::White, - false, Piece::Black, Some(0), )); @@ -510,7 +510,6 @@ mod tests { 0, Board::new().starting_pos(), Piece::Black, - false, Piece::Black, None, )); @@ -546,10 +545,10 @@ mod tests { winner: Winner::None, parent: None, children: vec![1], - value: 0, + value: None, self_value: 0, color: Piece::Black, - lazy_children: false, + is_lazy: false, }); futm.update_root_idx_raw(0); @@ -560,7 +559,6 @@ mod tests { 0, Board::new(), Piece::White, - false, Piece::Black, Some(0), )); @@ -573,7 +571,6 @@ mod tests { 1234, Board::new(), Piece::White, - false, Piece::Black, None, )); @@ -583,7 +580,6 @@ mod tests { 0, Board::new(), Piece::White, - false, Piece::Black, Some(0), )); @@ -595,11 +591,68 @@ mod tests { 0, Board::new(), Piece::White, - false, Piece::Black, Some(0), )); assert_eq!(futm.depth_of(3), 2); } + + #[test] + fn by_depth_test() { + let mut futm = FutureMoves::new(Piece::Black, 0, 0); + + futm.arena.push(Move { + i: 0, + j: 0, + board: Board::new(), + winner: Winner::None, + parent: None, + children: vec![1], + value: None, + self_value: 0, + color: Piece::Black, + is_lazy: false, + }); + + futm.update_root_idx_raw(0); + + // child 1 + futm.arena.push(Move::new( + 0, + 0, + Board::new(), + Piece::White, + Piece::Black, + Some(0), + )); + futm.arena[1].parent = Some(0); + futm.arena[1].children = vec![3]; + + // dummy + futm.arena.push(Move::new( + 1234, + 1234, + Board::new(), + Piece::White, + Piece::Black, + None, + )); + + futm.arena.push(Move::new( + 0, + 0, + Board::new(), + Piece::White, + Piece::Black, + Some(0), + )); + + futm.arena[3].parent = Some(1); + + assert_eq!( + futm.by_depth(0..futm.arena.len()), + vec![(0, vec![0, 2]), (1, vec![1]), (2, vec![3])] + ); + } } diff --git a/src/logic/move.rs b/src/logic/move.rs index 9e8979d..03f3273 100644 --- a/src/logic/move.rs +++ b/src/logic/move.rs @@ -23,7 +23,7 @@ pub struct Move { pub children: Vec, /// Value of this move (including children) - pub value: i128, + pub value: Option, /// What is the inherit value of this move (not including children) pub self_value: i64, @@ -31,8 +31,8 @@ pub struct Move { /// Which color made a move on this move? pub color: Piece, - /// Should the children of this move be lazily generated? - pub lazy_children: bool, + /// Was this child lazily created? (it will have delayed child generation) + pub is_lazy: bool, } lazy_static! { @@ -45,7 +45,6 @@ impl Move { j: usize, board: Board, color: Piece, - lazy_children: bool, agent_color: Piece, parent: Option, ) -> Self { @@ -56,13 +55,13 @@ impl Move { winner: board.game_winner(), parent, children: Vec::new(), - value: 0, + value: None, color, - lazy_children, + is_lazy: false, self_value: 0, }; m.self_value = m.compute_self_value(agent_color); - m.value = m.self_value as i128; + m.value = Some(m.self_value as i128); m } @@ -79,11 +78,14 @@ impl Move { } else if self.winner == Winner::Player(agent_color) { // results in a win for the agent return i64::MAX - 1; - } else if self.winner == Winner::Tie { - // idk what a Tie should be valued? - return 0; } + // else if self.winner == Winner::Tie { + // // idk what a Tie should be valued? + // return 0; + // } + // I guess ignore Ties here, don't give them an explicit value, + // because even in the case of ties, we want to have a higher score BVM.board_value(&self.board, agent_color) }