diff --git a/Cargo.lock b/Cargo.lock index 63edff8..776e2ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + [[package]] name = "arrayvec" version = "0.7.6" @@ -44,12 +65,70 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "console" version = "0.15.10" @@ -63,6 +142,73 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[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 = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "either" version = "1.13.0" @@ -93,6 +239,22 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "indicatif" version = "0.17.11" @@ -106,6 +268,32 @@ dependencies = [ "web-time", ] +[[package]] +name = "is-terminal" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + [[package]] name = "js-sys" version = "0.3.77" @@ -134,6 +322,12 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "num" version = "0.4.3" @@ -219,12 +413,19 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + [[package]] name = "othello" version = "0.1.0" dependencies = [ "arrayvec", "bitvec", + "criterion", "either", "indicatif", "lazy_static", @@ -233,6 +434,34 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.10.0" @@ -303,6 +532,108 @@ dependencies = [ "zerocopy 0.8.18", ] +[[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 = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -326,6 +657,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "unicode-ident" version = "1.0.16" @@ -338,6 +679,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" @@ -355,6 +706,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] @@ -404,6 +756,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -414,6 +776,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 187420b..da379e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,15 @@ name = "othello" version = "0.1.0" edition = "2021" +[lib] +name = "othello" +path = "src/lib.rs" + +[[bin]] +name = "othello_game" +path = "src/main.rs" + + [profile.release] # for profiling debug = true @@ -18,3 +27,10 @@ lazy_static = "1.5" num = "0.4" rand = "0.9" static_assertions = "1.1" + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "future_children" +harness = false diff --git a/benches/future_children.rs b/benches/future_children.rs new file mode 100644 index 0000000..63cb64b --- /dev/null +++ b/benches/future_children.rs @@ -0,0 +1,24 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use std::hint::black_box; + +// use crate::future_move::FutureMove; +use othello::{board::Board, future_moves::FutureMoves, piece::Piece}; + +fn future_move_bench(depth: usize, expire: usize) { + let mut fut = FutureMoves::new(Piece::Black, depth, expire); + fut.update(&Board::new().starting_pos()); + let _best_move = fut.best_move().inspect(|&(i, j)| { + if !fut.update_root_coord(i, j) { + panic!("update_root_coord failed"); + } + }); +} + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("depth 6 expire 4", |b| { + b.iter(|| future_move_bench(black_box(6), black_box(4))) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/complexagent.rs b/src/complexagent.rs index fdd2a26..b0a752e 100644 --- a/src/complexagent.rs +++ b/src/complexagent.rs @@ -1,383 +1,4 @@ -use crate::{ - agent::Agent, - board::{Board, Winner}, - piece::Piece, -}; -use indicatif::{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, - - 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 { - let mut self_value = self.board.net_score(agent_color) as 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 - 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` or 0? - - self_value - } -} - -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, - - /// 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, - } - } - - const LAZY_EXPIRE: usize = 6; - - /// Generate children for all children of `nodes` - 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| self.arena[idx].children.is_empty() || self.arena[idx].lazy_children) - .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 < Self::LAZY_EXPIRE - { - false - } else { - // this is a non-lazy_children child, should it be lazy? - 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> { - // early-exit if a winner for the parent already exists - if self.arena[parent_idx].winner != Winner::None { - return None; - } - - let new_color = !self.arena[parent_idx].color; - let parent_lazy = self.arena[parent_idx].lazy_children; - 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)| self.arena[parent_idx].board.what_if(i, j, new_color).map(|x| (i, j, x))) - .map(|(i, j, new_board)| Move { - i, - j, - board: new_board, - winner: new_board.game_winner(!self.arena[parent_idx].color), - 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 { - // TODO! Move this to `extend_layers` so we can prune based on recursive [`Move`] value - - new.drain(TOP_K_CHILDREN..); - } - - let start_idx = self.arena.len(); - if parent_lazy && !lazy_children { - // this move's children are being regenerated after lazy child expiration, don't append first node - if new.len() > 1 { - self.arena.extend(new.drain(1..)); - } 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 self_value = - self.arena[idx].compute_self_value(self.agent_color) / (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].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 { - 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, - lazy_children: false, - }); - self.update_root_idx(0); - } - } - - /// 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() - .filter(|(_, node)| { - node.parent == self.current_root - && self.current_root.is_some() - && node.i == i - && node.j == j - }) - .next() - .map(|x| x.0) - .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) - 1; - 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]; - } -} +use crate::{agent::Agent, board::Board, future_moves::FutureMoves, piece::Piece}; pub struct ComplexAgent { color: Piece, @@ -386,10 +7,10 @@ pub struct ComplexAgent { impl ComplexAgent { pub const fn new(color: Piece) -> Self { - const MAX_DEPTH: usize = 10; + const MAX_DEPTH: usize = 15; Self { color, - future_moves: FutureMoves::new(color, MAX_DEPTH), + future_moves: FutureMoves::new(color, MAX_DEPTH, 4), } } } @@ -398,7 +19,7 @@ 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()); + println!("# of moves stored: {}", self.future_moves.len()); self.future_moves.best_move().inspect(|&(i, j)| { if !self.future_moves.update_root_coord(i, j) { diff --git a/src/future_moves.rs b/src/future_moves.rs new file mode 100644 index 0000000..cd75901 --- /dev/null +++ b/src/future_moves.rs @@ -0,0 +1,381 @@ +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 { + let mut self_value = self.board.net_score(agent_color) as 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 + 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` or 0? + + 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() + } + + /// Generate children for all children of `nodes` + 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| self.arena[idx].children.is_empty() || self.arena[idx].lazy_children) + .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> { + // early-exit if a winner for the parent already exists + if self.arena[parent_idx].winner != Winner::None { + return None; + } + + let new_color = !self.arena[parent_idx].color; + let parent_lazy = self.arena[parent_idx].lazy_children; + 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)| self.arena[parent_idx].board.what_if(i, j, new_color).map(|x| (i, j, x))) + .map(|(i, j, new_board)| Move { + i, + j, + board: new_board, + winner: new_board.game_winner(!self.arena[parent_idx].color), + 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 && !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() > 1 { + self.arena.extend(new.drain(1..)); + } 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 self_value = + self.arena[idx].compute_self_value(self.agent_color) / (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].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 { + // 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, + lazy_children: false, + }); + self.update_root_idx(0); + } + } + + /// 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.i == i + && node.j == j + }) + .map(|x| x.0) + .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) - 1; + 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]; + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ef5ad5a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +mod agent; +mod bitboard; +pub mod board; +mod complexagent; +pub mod future_moves; +mod game; +mod misc; +pub mod piece; diff --git a/src/main.rs b/src/main.rs index a0b4902..8bbeee9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod agent; mod bitboard; mod board; mod complexagent; +pub mod future_moves; mod game; mod misc; mod piece;