From 204ba85202c49bf3abe45c0934cd21047f738df1 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Tue, 4 Mar 2025 13:18:17 -0500 Subject: [PATCH] overhaul chain system This made the code so fast, that the `board` benchmark became completely useless. So that benchmark was removed. Overall, we're looking at a futher 20% performance increase in `future_moves` --- Cargo.lock | 40 ------------- Cargo.toml | 8 --- benches/board.rs | 29 --------- src/elo.rs | 4 +- src/main.rs | 5 +- src/repr/bitboard.rs | 139 +++++++++++++++++++++++++++++++------------ src/repr/board.rs | 124 +++++++++++++++++++++++++------------- src/repr/chains.rs | 36 ----------- src/repr/misc.rs | 95 ----------------------------- src/repr/mod.rs | 5 +- 10 files changed, 190 insertions(+), 295 deletions(-) delete mode 100644 benches/board.rs delete mode 100644 src/repr/misc.rs diff --git a/Cargo.lock b/Cargo.lock index 9df2f82..7cf026b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,18 +41,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "bumpalo" version = "3.17.0" @@ -227,12 +215,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "getrandom" version = "0.3.1" @@ -430,7 +412,6 @@ name = "othello" version = "0.1.0" dependencies = [ "arrayvec", - "bitvec", "const_fn", "criterion", "either", @@ -504,12 +485,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.9.0" @@ -665,12 +640,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tinytemplate" version = "1.2.1" @@ -881,15 +850,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index bf64179..5e93ef8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,12 +21,8 @@ inherits = "release" # for profiling debug = true -[features] -bitvec = [ "dep:bitvec" ] - [dependencies] arrayvec = "0.7" -bitvec = { version = "1", optional = true } const_fn = "0.4" either = "1.13" indicatif = "0.17" @@ -44,10 +40,6 @@ criterion = { version = "0.5", features = [ "html_reports" ] } name = "future_children" harness = false -[[bench]] -name = "board" -harness = false - [lints.rust] # fix weird warnings about `test` not being expected unexpected_cfgs = { level = "allow", check-cfg = ['cfg(test)'] } diff --git a/benches/board.rs b/benches/board.rs deleted file mode 100644 index 5ee12d7..0000000 --- a/benches/board.rs +++ /dev/null @@ -1,29 +0,0 @@ -use criterion::{criterion_group, criterion_main, Criterion, Throughput}; -use othello::repr::{Board, Piece}; - -fn fill_board(loops: usize) { - let mut board = Board::new(); - board.place_unchecked((0, 1).into(), Piece::White); - board.place_unchecked((0, 2).into(), Piece::Black); - board.place_unchecked((0, 3).into(), Piece::Black); - board.place_unchecked((0, 4).into(), Piece::Black); - for _ in 0..loops { - let mut board = board; - let _ = board.place((0, 5).into(), Piece::White); - } -} - -fn criterion_benchmark(c: &mut Criterion) { - let mut group = c.benchmark_group("board"); - - const LOOPS: usize = 1000; - - group.throughput(Throughput::Elements(LOOPS as u64)); - group.bench_function("board_place", |b| { - b.iter(|| fill_board(LOOPS)); - }); - group.finish(); -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/src/elo.rs b/src/elo.rs index 5ec8224..35458c3 100644 --- a/src/elo.rs +++ b/src/elo.rs @@ -135,8 +135,8 @@ impl PlayerArena { } fn create_agents( - player_1_fn: &Box Box>, - player_2_fn: &Box Box>, + player_1_fn: &dyn Fn(Piece) -> Box, + player_2_fn: &dyn Fn(Piece) -> Box, ) -> (Box, Box) { (player_1_fn(Piece::Black), player_2_fn(Piece::White)) } diff --git a/src/main.rs b/src/main.rs index 14080ae..212df48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use elo::run; use game::Game; use logic::{ChildrenEvalMethod, FutureMoveConfig}; use repr::Piece; @@ -13,8 +12,8 @@ pub mod repr; // TODO! make this agent configuration a config option via `clap-rs` fn main() { - run(); - return; + // run(); + // return; let player1 = complexagent::ComplexAgent::new( Piece::Black, FutureMoveConfig { diff --git a/src/repr/bitboard.rs b/src/repr/bitboard.rs index 7436e0b..ee22f00 100644 --- a/src/repr/bitboard.rs +++ b/src/repr/bitboard.rs @@ -1,25 +1,10 @@ use super::{ board::Board, coords::{CoordPair, CoordPairInner}, + CoordAxis, }; -use const_fn::const_fn; use static_assertions::const_assert; -// quick explanation for the dual-nature of [`BitBoard`] -// There's both a `bitvec` impl (which is variable length) -// and a `native` impl which uses a u64 as the backing type -// the `native` impl is ~15-25% faster (in non-BitBoard specific benchmarks) -// `bitvec` is only really useful if you're using esoteric board sizes - -#[cfg(feature = "bitvec")] -use bitvec::prelude::*; - -#[cfg(feature = "bitvec")] -type BBBaseType = u64; - -#[cfg(feature = "bitvec")] -pub type BitBoardInner = BitArr!(for Board::BOARD_AREA, in BBBaseType, Lsb0); - #[cfg(not(feature = "bitvec"))] pub type BitBoardInner = u64; @@ -30,55 +15,133 @@ pub struct BitBoard(BitBoardInner); const_assert!(std::mem::size_of::() * 8 >= Board::BOARD_AREA as usize); impl BitBoard { - #[cfg(feature = "bitvec")] - #[allow(clippy::new_without_default)] - pub const fn new() -> Self { - Self(bitarr!(BBBaseType, Lsb0; 0; Board::BOARD_AREA)) - } - - #[cfg(not(feature = "bitvec"))] #[allow(clippy::new_without_default)] pub const fn new() -> Self { Self(0) } - #[const_fn(cfg(not(feature = "bitvec")))] pub const fn get(&self, coord: CoordPair) -> bool { self.get_by_index(coord.0) } - #[const_fn(cfg(not(feature = "bitvec")))] pub const fn set(&mut self, coord: CoordPair, value: bool) { self.set_by_index(coord.0, value); } - #[cfg(not(feature = "bitvec"))] const fn get_by_index(&self, index: CoordPairInner) -> bool { ((self.0 >> index) & 0b1) != 0b0 } - #[cfg(not(feature = "bitvec"))] const fn set_by_index(&mut self, index: CoordPairInner, value: bool) { // PERF! branchless setting of bit (~+3% perf bump) self.0 &= !(0b1 << index); // clear bit self.0 |= (value as BitBoardInner) << index; // set bit (if needed) } - #[cfg(feature = "bitvec")] - pub fn get_by_index(&self, index: CoordPairInner) -> bool { - self.0[index as usize] - } - - #[cfg(feature = "bitvec")] - pub fn set_by_index(&mut self, index: CoordPairInner, value: bool) { - self.0.set(index as usize, value); - } - // works on both `bitvec` and native (const on native) - #[const_fn(cfg(not(feature = "bitvec")))] pub const fn count(&self) -> usize { self.0.count_ones() as usize } + + // Directional shifts with edge masking (prevents wrapping) + pub const fn east(&self) -> Self { + let mask = !Self::col_mask(Board::BOARD_SIZE).0; // Mask to block column 7 bits + Self((self.0 & mask) << 1) + } + + pub const fn west(&self) -> Self { + let mask = !Self::col_mask(0).0; + Self((self.0 & mask) >> 1) + } + + pub const fn north(&self) -> Self { + Self(self.0 >> Board::BOARD_SIZE) + } + + pub const fn south(&self) -> Self { + Self(self.0 << Board::BOARD_SIZE) + } + + pub const fn northeast(&self) -> Self { + self.north().east() + } + + pub const fn northwest(&self) -> Self { + self.north().west() + } + + pub const fn southeast(&self) -> Self { + self.south().east() + } + + pub const fn southwest(&self) -> Self { + self.south().west() + } + + // Mask for a specific column (e.g., col_mask(7) = 0x8080808080808080) + const fn col_mask(col: CoordAxis) -> Self { + let mut mask = 0; + let mut i = 0; + while i < Board::BOARD_AREA { + mask |= 1 << (i + col); + i += Board::BOARD_SIZE; + } + Self(mask) + } + + // Check if a BitBoard contains a coordinate + pub const fn contains(&self, coord: CoordPair) -> bool { + (self.0 & (1 << coord.0)) != 0 + } + + // Create a BitBoard from a single coordinate + pub const fn from_coord(coord: CoordPair) -> Self { + Self(1 << coord.0) + } + + pub fn intersects(self, other: Self) -> bool { + (self & other).count() > 0 + } + + pub fn union(self, other: Self) -> Self { + self | other + } +} + +impl std::ops::Not for BitBoard { + type Output = BitBoard; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl std::ops::BitAnd for BitBoard { + type Output = BitBoard; + + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } +} + +impl std::ops::BitOr for BitBoard { + type Output = BitBoard; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl std::ops::BitAndAssign for BitBoard { + fn bitand_assign(&mut self, rhs: Self) { + *self = *self & rhs; + } +} + +impl std::ops::BitOrAssign for BitBoard { + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } } #[cfg(test)] diff --git a/src/repr/board.rs b/src/repr/board.rs index 59a77c7..cbef688 100644 --- a/src/repr/board.rs +++ b/src/repr/board.rs @@ -1,12 +1,44 @@ -use super::{ - bitboard::BitBoard, - chains::{gen_adj_lookup, ChainCollection, PosMap}, - piece::Piece, - CoordAxis, CoordPair, -}; +use super::{bitboard::BitBoard, piece::Piece, CoordAxis, CoordPair}; +use arrayvec::ArrayVec; use const_fn::const_fn; use rand::seq::IteratorRandom; -use std::{cmp::Ordering, fmt, sync::LazyLock}; +use std::{cmp::Ordering, fmt}; + +/// Map of all points on the board against some type T +/// Used to index like so: example[i][j] +/// with each coordinate +pub struct PosMap(ArrayVec); + +impl PosMap { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(ArrayVec::from_iter( + (0..Board::BOARD_AREA).map(|_| Default::default()), + )) + } + + pub fn get(&self, coords: CoordPair) -> &T { + &self.0[coords.0 as usize] + } + + pub fn set(&mut self, coords: CoordPair, value: T) { + self.0[coords.0 as usize] = value; + } +} + +type PosMapOrig = [[T; Board::BOARD_SIZE as usize]; Board::BOARD_SIZE as usize]; + +impl From> for PosMap { + fn from(value: PosMapOrig) -> Self { + let mut new = Self::new(); + for i in 0..Board::BOARD_SIZE { + for j in 0..Board::BOARD_SIZE { + new.set((i, j).into(), value[i as usize][j as usize]); + } + } + new + } +} #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum Winner { @@ -15,9 +47,6 @@ pub enum Winner { None, } -/// Precompute all possible chains for each position on the board -static ADJ_LOOKUP: LazyLock> = LazyLock::new(gen_adj_lookup); - /// Repersents a Othello game board at a certain space #[derive(Copy, Clone, PartialEq, Eq)] pub struct Board { @@ -205,7 +234,7 @@ impl Board { /// Returns a bool which represents whether or not a move would propegate and be valid pub fn would_prop(&self, coord: CoordPair, piece: Piece) -> bool { - self.get(coord).is_none() && self.propegate_from_dry(coord, piece).next().is_some() + self.get(coord).is_none() && self.propegate_from_dry(coord, piece).count() > 0 } pub fn place(&mut self, coord: CoordPair, piece: Piece) -> Result<(), &'static str> { @@ -228,19 +257,12 @@ impl Board { return 0; }; - // PERF! avoid clones and collections here using raw pointers - let iterator = unsafe { - // SAFETY! `propegate_from_dry` should not have overlapping chains - // if overlapping chains were to exist, `self.place_unchecked` could collide with `self.get` - // I now have a check in `ADJ_LOOKUP` on creation - (*(self as *const Self)).propegate_from_dry(coord, starting_color) - }; + let flip_mask = self.propegate_from_dry(coord, starting_color); + let count = flip_mask.count(); - let mut count = 0; - for &coord in iterator { - self.place_unchecked(coord, starting_color); - count += 1; - } + // Apply the flips + *self.board_mut(starting_color) |= flip_mask; + *self.board_mut(starting_color.flip()) &= !flip_mask; count } @@ -248,24 +270,46 @@ impl Board { /// Propegate piece captures originating from (i, j) /// DO NOT USE THIS ALONE, this should be called as a part of /// [`Board::place`] or [`Board::place_and_prop_unchecked`] - fn propegate_from_dry( - &self, - coords: CoordPair, - starting_color: Piece, - ) -> impl Iterator + use<'_> { - ADJ_LOOKUP - .get(coords) - .iter() - .flat_map(move |chain| { - for (idx, &coord) in chain.into_iter().enumerate() { - let piece = self.get(coord)?; - if piece == starting_color { - return chain.get(..idx); - } + fn propegate_from_dry(&self, coords: CoordPair, starting_color: Piece) -> BitBoard { + let opponent_color = starting_color.flip(); + let player_board = *self.board(starting_color); + let opponent_board = *self.board(opponent_color); + + let mut flip_mask = BitBoard::new(); + let seed = BitBoard::from_coord(coords); + + // Directions to check: east, west, north, south, and diagonals + let directions = [ + BitBoard::east, + BitBoard::west, + BitBoard::north, + BitBoard::south, + BitBoard::northeast, + BitBoard::northwest, + BitBoard::southeast, + BitBoard::southwest, + ]; + + for dir in directions { + let mut current = seed; + let mut temp_flips = BitBoard::new(); + + // Expand in direction until edge or non-opponent piece + loop { + current = dir(¤t); + if current.count() == 0 || !current.intersects(opponent_board) { + break; } - None - }) - .flatten() + temp_flips = temp_flips.union(current); + } + + // If terminated on a player piece, keep the flips + if current.intersects(player_board) { + flip_mask = flip_mask.union(temp_flips); + } + } + + flip_mask } /// Count the number of a type of [`Piece`] on the board diff --git a/src/repr/chains.rs b/src/repr/chains.rs index 9783986..6c2bec3 100644 --- a/src/repr/chains.rs +++ b/src/repr/chains.rs @@ -11,42 +11,6 @@ type Chain = ArrayVec; /// A collection of chains (up vert, down vert, left horiz, right horiz, diagonals....) pub type ChainCollection = ArrayVec; -/// Map of all points on the board against some type T -/// Used to index like so: example[i][j] -/// with each coordinate -pub struct PosMap(ArrayVec); - -impl PosMap { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self(ArrayVec::from_iter( - (0..Board::BOARD_AREA).map(|_| Default::default()), - )) - } - - pub fn get(&self, coords: CoordPair) -> &T { - &self.0[coords.0 as usize] - } - - pub fn set(&mut self, coords: CoordPair, value: T) { - self.0[coords.0 as usize] = value; - } -} - -type PosMapOrig = [[T; Board::BOARD_SIZE as usize]; Board::BOARD_SIZE as usize]; - -impl From> for PosMap { - fn from(value: PosMapOrig) -> Self { - let mut new = Self::new(); - for i in 0..Board::BOARD_SIZE { - for j in 0..Board::BOARD_SIZE { - new.set((i, j).into(), value[i as usize][j as usize]); - } - } - new - } -} - /// Creates a lookup map for adjacencies and chains from each position on the board pub fn gen_adj_lookup() -> PosMap { PosMap( diff --git a/src/repr/misc.rs b/src/repr/misc.rs deleted file mode 100644 index 2469a68..0000000 --- a/src/repr/misc.rs +++ /dev/null @@ -1,95 +0,0 @@ -use either::Either; -use std::{iter::Rev, ops::RangeInclusive}; - -pub fn split_from(range: RangeInclusive, x: T) -> [impl Iterator + Clone; 2] -where - T: num::Integer + Copy, - RangeInclusive: Iterator + DoubleEndedIterator, - Rev>: Iterator, -{ - let in_range = range.contains(&x); - let (start, end) = (*range.start(), *range.end()); - - // RangeInclusive (1..=0), has 0 elements - let base = Either::Right(T::one()..=T::zero()); - [ - if in_range && x > start + T::one() { - Either::Left((start..=(x - T::one())).rev()) - } else { - base.clone() - }, - if in_range && x + T::one() < end { - Either::Right((x + T::one())..=end) - } else { - base - }, - ] -} - -pub fn diag_raw( - i_chains: [impl Iterator + Clone; 2], - j_chains: [impl Iterator + Clone; 2], -) -> [impl Iterator + Clone; 4] -where - T: num::Integer + Copy, - RangeInclusive: Iterator + DoubleEndedIterator, - Rev>: Iterator, -{ - [(0, 0), (1, 1), (1, 0), (0, 1)].map(move |(a, b)| i_chains[a].clone().zip(j_chains[b].clone())) -} - -#[cfg(test)] -mod test { - use super::*; - - fn diag_test_helper( - i: T, - j: T, - range_i: RangeInclusive, - range_j: RangeInclusive, - ) -> [impl Iterator + Clone; 4] - where - T: num::Integer + Copy, - RangeInclusive: Iterator + DoubleEndedIterator, - Rev>: Iterator, - { - diag_raw(split_from(range_i, i), split_from(range_j, j)) - } - - #[test] - fn split_test() { - assert_eq!( - split_from(0..=6, 2).map(Iterator::collect::>), - [vec![1, 0], vec![3, 4, 5, 6]] - ); - - assert_eq!( - split_from(0..=6, 0).map(Iterator::collect::>), - [vec![], vec![1, 2, 3, 4, 5, 6]] - ); - - assert_eq!( - split_from(0..=6, 6).map(Iterator::collect::>), - [vec![5, 4, 3, 2, 1, 0], vec![]] - ); - - // test out-of-bounds and also generics - assert_eq!( - split_from::(-1i16..=4i16, 10i16).map(Iterator::collect::>), - [const { Vec::::new() }; 2] - ); - } - - #[test] - fn diag_test() { - assert_eq!( - diag_test_helper(2, 3, 0..=7, 0..=7).map(Iterator::collect::>), - [ - vec![(1, 2), (0, 1)], - vec![(3, 4), (4, 5), (5, 6), (6, 7)], - vec![(3, 2), (4, 1), (5, 0)], - vec![(1, 4), (0, 5)] - ] - ); - } -} diff --git a/src/repr/mod.rs b/src/repr/mod.rs index 0c4fdb7..0fafc99 100644 --- a/src/repr/mod.rs +++ b/src/repr/mod.rs @@ -1,11 +1,8 @@ mod bitboard; mod board; -mod chains; mod coords; -mod misc; mod piece; -pub use board::{Board, Winner}; -pub use chains::PosMap; +pub use board::{Board, PosMap, Winner}; pub use coords::{CoordAxis, CoordPair}; pub use piece::Piece;