use super::{bitboard::BitBoard, piece::Piece, CoordAxis, CoordPair}; use arrayvec::ArrayVec; use rand::seq::IteratorRandom; 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::AREA.0).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::SIZE as usize]; Board::SIZE as usize]; impl From> for PosMap { fn from(value: PosMapOrig) -> Self { let mut new = Self::new(); for i in 0..Board::SIZE { for j in 0..Board::SIZE { new.set((i, j).into(), value[i as usize][j as usize]); } } new } } #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum Winner { Player(Piece), Tie, None, } macro_rules! get_board { // Immutable static access ($self:expr, Piece::White) => { $self.white_board }; ($self:expr, Piece::Black) => { $self.black_board }; // Mutable static access (mut $self:expr, Piece::White) => { $self.white_board }; (mut $self:expr, Piece::Black) => { $self.black_board }; // Immutable dynamic access ($self:expr, $piece:expr) => {{ match $piece { Piece::White => &$self.white_board, Piece::Black => &$self.black_board, } }}; // Mutable dynamic access (mut $self:expr, $piece:expr) => {{ match $piece { Piece::White => &mut $self.white_board, Piece::Black => &mut $self.black_board, } }}; } /// Repersents a Othello game board at a certain space #[derive(Copy, Clone, PartialEq, Eq)] pub struct Board { /// [`BitBoard`] containing all white pieces white_board: BitBoard, /// [`BitBoard`] containing all black pieces black_board: BitBoard, } impl fmt::Display for Board { #[allow(clippy::repeat_once)] // clippy gets mad about when PADDING == 1 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let horiz_sep_line = "-".repeat((Self::SIZE * 2 + 1) as usize); // basically calculates the # of digits BOARD_SIZE needs const PADDING: usize = (Board::SIZE - 1).ilog10() as usize + 1; let space_padding = " ".repeat(PADDING); // Print numbers at top so the board can be read more easier write!(f, "{} ", space_padding)?; for j in (0..Self::SIZE).rev() { write!(f, "{:0PADDING$} ", j)?; } writeln!(f)?; for i in (0..Self::SIZE).rev() { writeln!(f, "{}{}", space_padding, horiz_sep_line)?; write!(f, "{:0PADDING$}|", i)?; for j in (0..Self::SIZE).rev() { write!( f, "{}|", self.get((i, j).into()) .as_ref() .map(Piece::symbol) .unwrap_or(' ') )?; } writeln!(f)?; } // put a line at the bottom of the board too writeln!(f, " {}", horiz_sep_line)?; // Print the current score write!( f, "{}", [Piece::White, Piece::Black] .map(|p| format!("{} Score: {}\n", p.text(), self.count(p))) .concat() )?; Ok(()) } } impl fmt::Debug for Board { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self) } } impl Board { /// Width of the board pub const SIZE: CoordAxis = 8; /// Area of the board pub const AREA: CoordPair = CoordPair::area(Self::SIZE); /// Create a new empty board #[allow(clippy::new_without_default)] pub const fn new() -> Self { Self { white_board: BitBoard::new(), black_board: BitBoard::new(), } } pub fn random(steps: usize) -> Self { let mut new = Self::new().starting_pos(); let mut p = Piece::Black; let mut rng = rand::rng(); for _ in 0..steps { if let Some(m) = new.possible_moves(p).choose(&mut rng) { let _ = new.place(m, p); } p = !p; } new } /// Starting position pub const fn starting_pos(mut self) -> Self { let hf = Self::SIZE / 2; self.place_unchecked(CoordPair::from_axes(hf - 1, hf - 1), Piece::White); self.place_unchecked(CoordPair::from_axes(hf, hf - 1), Piece::Black); self.place_unchecked(CoordPair::from_axes(hf - 1, hf), Piece::Black); self.place_unchecked(CoordPair::from_axes(hf, hf), Piece::White); self } /// Provides an iterator of all possible positions on the board pub fn all_positions() -> impl Iterator { (0..Self::AREA.0).map(CoordPair) } /// Returns an iterator of all possible moves a `color` can make pub fn possible_moves(&self, color: Piece) -> impl Iterator + use<'_> { Self::all_positions().filter(move |&coord| self.would_prop(coord, color)) } pub const fn get_piece(&self, coord: CoordPair, color: Piece) -> bool { get_board!(self, color).get(coord) } /// Returns the color of a place on the [`Board`] at a position pub const fn get(&self, coord: CoordPair) -> Option { if self.get_piece(coord, Piece::White) { Some(Piece::White) } else if self.get_piece(coord, Piece::Black) { Some(Piece::Black) } else { None } } /// Place a piece without checking for propegation of validity /// only pub for setting up benchmark pub const fn place_unchecked(&mut self, coord: CoordPair, piece: Piece) { let is_white = matches!(piece, Piece::White); get_board!(self, Piece::White).set(coord, is_white); get_board!(self, Piece::Black).set(coord, !is_white); } /// Return a modified [`Board`] with the piece placed at a position /// Returns None if the move was invalid pub const fn what_if(&self, coord: CoordPair, piece: Piece) -> Option { // extract check here to avoid copy if self.get(coord).is_some() { return None; } let mut self_copy = *self; if let Ok(_) = self_copy.place(coord, piece) { Some(self_copy) } else { None } } /// Returns a bool which represents whether or not a move would propegate and be valid pub const fn would_prop(&self, coord: CoordPair, piece: Piece) -> bool { self.get(coord).is_none() && !self.propegate_from_dry(coord, piece).is_empty() } pub const fn place(&mut self, coord: CoordPair, piece: Piece) -> Result<(), &'static str> { if self.get(coord).is_some() { return Err("position is occupied"); } if self.propegate_from(coord, piece) { self.place_unchecked(coord, piece); Ok(()) } else { Err("move would not propegate") } } /// Propegate the board and captures starting from a specific position /// returns true if flips occurred const fn propegate_from(&mut self, coord: CoordPair, starting_color: Piece) -> bool { let flip_mask = self.propegate_from_dry(coord, starting_color); let did_flip = !flip_mask.is_empty(); // Apply the flips self.apply_flip_mask(starting_color, flip_mask); did_flip } const fn apply_flip_mask(&mut self, color: Piece, flip_mask: BitBoard) { // did some investigation, seems using xor actually decreases // performance over the branchfull impl? 3.2-3.5% slower get_board!(mut self, color).bitor_assign(flip_mask); get_board!(mut self, color.flip()).bitand_assign(flip_mask.not()); } /// 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`] const fn propegate_from_dry(&self, coords: CoordPair, starting_color: Piece) -> BitBoard { let player_board = get_board!(self, starting_color); let opponent_board = get_board!(self, starting_color.flip()); let mut flip_mask = BitBoard::new(); let seed = BitBoard::from_coord(coords); macro_rules! apply_dir { ($base:expr, $sum_mask:expr, $dir:expr) => { let mut current = $base; let mut temp_flips = BitBoard::new(); // Expand in direction until edge or non-opponent piece loop { current = $dir(¤t, 1); if current.is_empty() || !current.intersects(*opponent_board) { break; } temp_flips.bitor_assign(current); } // If terminated on a player piece, keep the flips if current.intersects(*player_board) { $sum_mask.bitor_assign(temp_flips); } }; } apply_dir!(seed, flip_mask, BitBoard::east); apply_dir!(seed, flip_mask, BitBoard::west); apply_dir!(seed, flip_mask, BitBoard::north); apply_dir!(seed, flip_mask, BitBoard::south); apply_dir!(seed, flip_mask, BitBoard::northeast); apply_dir!(seed, flip_mask, BitBoard::northwest); apply_dir!(seed, flip_mask, BitBoard::southeast); apply_dir!(seed, flip_mask, BitBoard::southwest); flip_mask } /// Count the number of a type of [`Piece`] on the board pub const fn count(&self, piece: Piece) -> usize { get_board!(self, piece).count() } /// Get the "net score" of a player /// Formula: `net_score = Score_player - Score_opponent` pub const fn net_score(&self, piece: Piece) -> i16 { self.count(piece) as i16 - self.count(piece.flip()) as i16 } /// Returns the winner of the board (if any) pub fn game_winner(&self) -> Winner { // Wikipedia: `Players take alternate turns. If one player cannot make a valid move, play passes back to the other player. The game ends when the grid has filled up or if neither player can make a valid move.` if self.possible_moves(Piece::Black).next().is_some() || self.possible_moves(Piece::White).next().is_some() { // player can still make a move, there is no winner return Winner::None; } match self.count(Piece::White).cmp(&self.count(Piece::Black)) { Ordering::Greater => Winner::Player(Piece::White), // White win Ordering::Less => Winner::Player(Piece::Black), // Black win Ordering::Equal => Winner::Tie, } } } #[cfg(test)] mod test { use super::*; #[test] fn place_and_get() { let mut board = Board::new(); assert_eq!(board.get((0, 0).into()), None); board.place_unchecked((0, 0).into(), Piece::Black); assert_eq!(board.get((0, 0).into()), Some(Piece::Black)); } #[test] fn place_and_capture_simple() { let mut board = Board::new(); board.place_unchecked((0, 0).into(), Piece::Black); board.place_unchecked((0, 1).into(), Piece::White); assert_eq!(board.place((0, 2).into(), Piece::Black), Ok(())); assert_eq!(board.get((0, 1).into()), Some(Piece::Black)); } #[test] fn failed_capture() { let mut board = Board::new(); board.place_unchecked((0, 0).into(), Piece::Black); board.place_unchecked((0, 2).into(), Piece::White); // should fail assert_ne!(board.place((0, 3).into(), Piece::Black), Ok(())); assert_eq!( board.get((0, 1).into()), None, "(0, 1) was overridden even though it's an empty space" ); } #[test] fn long_capture_horiz() { let mut board = Board::new(); board.place_unchecked((0, 0).into(), Piece::Black); for j in 1..=6 { board.place_unchecked((0, j).into(), Piece::White); } assert_eq!(board.place((0, 7).into(), Piece::Black), Ok(())); for j in 2..=6 { assert_eq!( board.get((0, j).into()), Some(Piece::Black), "should be black at: ({}, {})", 0, j ); } } #[test] fn long_capture_vert() { let mut board = Board::new(); board.place_unchecked((0, 0).into(), Piece::Black); for i in 1..=6 { board.place_unchecked((i, 0).into(), Piece::White); } assert_eq!(board.place((7, 0).into(), Piece::Black), Ok(())); for i in 2..=6 { assert_eq!( board.get((i, 0).into()), Some(Piece::Black), "should be black at: ({}, {})", i, 0 ); } } // Test corner capture from top-left corner #[test] fn corner_capture_top_left() { let mut board = Board::new(); // Black pieces at (2, 2) and (0, 0) board.place_unchecked((1, 1).into(), Piece::White); // to be captured board.place_unchecked((2, 2).into(), Piece::Black); assert_eq!(board.place((0, 0).into(), Piece::Black), Ok(())); // Capture white piece at (1,1) assert_eq!(board.get((1, 1).into()), Some(Piece::Black), "\n{}", board); } // Test corner capture from top-right corner #[test] fn corner_capture_top_right() { let mut board = Board::new(); // Black pieces at (0, 7) and (2, 5) board.place_unchecked((0, 7).into(), Piece::Black); board.place_unchecked((1, 6).into(), Piece::White); // to be captured assert_eq!(board.place((2, 5).into(), Piece::Black), Ok(())); // Capture white piece at (1, 6) assert_eq!(board.get((1, 6).into()), Some(Piece::Black), "\n{}", board); } // Test corner capture from bottom-left corner #[test] fn corner_capture_bottom_left() { let mut board = Board::new(); // Black pieces at (7, 0) and (5, 2) board.place_unchecked((7, 0).into(), Piece::Black); board.place_unchecked((6, 1).into(), Piece::White); // to be captured assert_eq!(board.place((5, 2).into(), Piece::Black), Ok(())); // Capture white piece at (6, 1) assert_eq!(board.get((6, 1).into()), Some(Piece::Black), "\n{}", board); } // Test corner capture from bottom-right corner #[test] fn corner_capture_bottom_right() { let mut board = Board::new(); // Black pieces at (7, 7) and (5, 5) board.place_unchecked((7, 7).into(), Piece::Black); board.place_unchecked((6, 6).into(), Piece::White); // to be captured assert_eq!(board.place((5, 5).into(), Piece::Black), Ok(())); // Capture white piece at (6, 6) assert_eq!(board.get((6, 6).into()), Some(Piece::Black), "\n{}", board); } // Test capture from top-left corner (horizontal) #[test] fn capture_top_left_horiz() { let mut board = Board::new(); // Create a scenario where a capture should happen horizontally from (0, 0) board.place_unchecked((0, 0).into(), Piece::Black); board.place_unchecked((0, 1).into(), Piece::White); // to be captured assert_eq!(board.place((0, 2).into(), Piece::Black), Ok(())); assert_eq!(board.get((0, 1).into()), Some(Piece::Black), "\n{}", board); } // Test capture from top-right corner (horizontal) #[test] fn capture_top_right_horiz() { let mut board = Board::new(); // Create a scenario where a capture should happen horizontally from (0, 7) board.place_unchecked((0, 7).into(), Piece::Black); board.place_unchecked((0, 6).into(), Piece::White); // to be captured assert_eq!(board.place((0, 5).into(), Piece::Black), Ok(())); assert_eq!(board.get((0, 6).into()), Some(Piece::Black), "\n{}", board); } // Test capture from top-left corner (vertical) #[test] fn capture_top_left_vert() { let mut board = Board::new(); // Create a scenario where a capture should happen vertically from (0, 0) board.place_unchecked((0, 0).into(), Piece::Black); board.place_unchecked((1, 0).into(), Piece::White); // to be captured assert_eq!(board.place((2, 0).into(), Piece::Black), Ok(())); assert_eq!(board.get((1, 0).into()), Some(Piece::Black), "\n{}", board); } // Test capture from bottom-left corner (vertical) #[test] fn capture_bottom_left_vert() { let mut board = Board::new(); // Create a scenario where a capture should happen vertically from (7, 0) board.place_unchecked((7, 0).into(), Piece::Black); board.place_unchecked((6, 0).into(), Piece::White); // to be captured assert_eq!(board.place((5, 0).into(), Piece::Black), Ok(())); assert_eq!(board.get((6, 0).into()), Some(Piece::Black), "\n{}", board); } }