use crate::{ misc::{diag, split_from}, piece::Piece, }; use std::{cmp::Ordering, fmt}; pub const BOARD_SIZE: usize = 8; #[derive(Copy, Clone, PartialEq, Eq)] pub struct Board { board: [[Option; BOARD_SIZE]; BOARD_SIZE], } impl fmt::Display for Board { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let horiz_sep_line = "-".repeat(BOARD_SIZE * 2 + 1); // Print numbers at top so I can read the board easier write!(f, " ")?; for j in 0..BOARD_SIZE { write!(f, "{} ", j)?; } writeln!(f)?; for i in 0..BOARD_SIZE { writeln!(f, " {}", horiz_sep_line)?; write!(f, "{}|", i)?; for j in 0..BOARD_SIZE { write!( f, "{}|", self.get(i, j).as_ref().map(Piece::text).unwrap_or(" ") )?; } writeln!(f)?; } // put a line at the bottom of the board too writeln!(f, " {}", horiz_sep_line)?; // Print the current score let (white_score, black_score) = self.get_score(); writeln!( f, "White Score: {}\nBlack Score: {}", white_score, black_score )?; // Print game over screen if self.game_over() { match self.get_winner() { Some(Piece::Black) => writeln!(f, "Black Wins"), Some(Piece::White) => writeln!(f, "White Wins"), None => writeln!(f, "Tie"), }? } Ok(()) } } impl Board { pub const fn new() -> Self { Self { board: [[None; BOARD_SIZE]; BOARD_SIZE], } } pub const fn starting_pos(mut self) -> Self { self.place_unchecked(3, 3, Piece::White); self.place_unchecked(4, 3, Piece::Black); self.place_unchecked(3, 4, Piece::Black); self.place_unchecked(4, 4, Piece::White); self } pub fn possible_moves(&self, color: Piece) -> Box + '_> { Box::new( (0..BOARD_SIZE) .flat_map(|i| (0..BOARD_SIZE).map(move |j| (i, j))) .filter(move |(i, j)| self.would_prop(*i, *j, color)), ) } /// Returns a mutable reference to a place on the [`Board`] /// at (i, j) pub const fn get_mut(&mut self, i: usize, j: usize) -> &mut Option { &mut self.board[i][j] } /// Returns a reference to a place on the [`Board`] /// at (i, j) pub const fn get(&self, i: usize, j: usize) -> &Option { &self.board[i][j] } const fn place_unchecked(&mut self, i: usize, j: usize, piece: Piece) { *self.get_mut(i, j) = Some(piece); } pub fn what_if(&self, i: usize, j: usize, piece: Piece) -> Option<(Self, usize)> { if self.get(i, j).is_some() { return None; } let mut self_copy = *self; self_copy.place_unchecked(i, j, piece); let how_many_prop = self_copy.propegate_from(i, j); if how_many_prop == 0 { return None; } Some((self_copy, how_many_prop)) } pub fn would_prop(&self, i: usize, j: usize, piece: Piece) -> bool { self.get(i, j).is_none() && !self.propegate_from_dry(i, j, piece).is_empty() } pub fn place(&mut self, i: usize, j: usize, piece: Piece) -> Result<(), String> { if let Some(what_if_result) = self.what_if(i, j, piece) { if what_if_result.1 > 0 { *self = what_if_result.0; return Ok(()); } } Err("move would not propegate".to_string()) } fn propegate_from(&mut self, i: usize, j: usize) -> usize { let Some(starting_color) = *self.get(i, j) else { return 0; }; let pos_s = self.propegate_from_dry(i, j, starting_color); let pos_len = pos_s.len(); for (i, j) in pos_s { self.place_unchecked(i, j, starting_color); } pos_len } /// 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, i: usize, j: usize, starting_color: Piece) -> Vec<(usize, usize)> { // Create all chains from the piece being propegated from in `i` and `j` coordinates let (i_chain, j_chain) = ( split_from(0, BOARD_SIZE - 1, i), split_from(0, BOARD_SIZE - 1, j), ); let mut chains: Vec> = i_chain .clone() .into_iter() .map(|range| range.into_iter().map(|i| (i, j)).collect()) .collect(); chains.extend( j_chain .clone() .into_iter() .map(|range| range.into_iter().map(|j| (i, j)).collect()), ); // handle diagonals chains.extend(diag(i, j, 0, 0, BOARD_SIZE - 1, BOARD_SIZE - 1)); let mut fill: Vec<(usize, usize)> = Vec::new(); for chain in chains { for (chain_length, &(new_i, new_j)) in chain.iter().enumerate() { let Some(piece) = self.get(new_i, new_j) else { // chain interupted by blank space break; }; if piece == &starting_color { // fill all opposite colors with this color let Some(history) = chain.get(..chain_length) else { break; }; // fill all opposite colors with this color for &(i_o, j_o) in history { fill.push((i_o, j_o)); } // either the other pieces were replaced, or this was an invalid chain, // in both cases, the loop needs to be breaked break; } } } fill } /// Returns (White score, Black score) pub fn get_score(&self) -> (usize, usize) { self.board .iter() .flatten() .flatten() .map(|cell| match cell { Piece::White => (1, 0), Piece::Black => (0, 1), }) .fold((0_usize, 0usize), |(a, b), (c, d)| (a + c, b + d)) } // Get the winning piece (for game over screen mainly) pub fn get_winner(&self) -> Option { let (white_score, black_score) = self.get_score(); match white_score.cmp(&black_score) { Ordering::Greater => Some(Piece::White), // White win Ordering::Less => Some(Piece::Black), // Black win Ordering::Equal => None, // Tie } } pub fn game_over(&self) -> bool { let (white_score, black_score) = self.get_score(); let max_score = BOARD_SIZE * BOARD_SIZE; let combined_score = black_score + white_score; max_score == combined_score } } #[cfg(test)] mod test { use super::*; #[test] fn place_and_get() { let mut board = Board::new(); assert_eq!(board.get(0, 0), &None); board.place_unchecked(0, 0, Piece::Black); assert_eq!(board.get(0, 0), &Some(Piece::Black)); } #[test] fn place_and_capture_simple() { let mut board = Board::new(); board.place_unchecked(0, 0, Piece::Black); board.place_unchecked(0, 1, Piece::White); board.place_unchecked(0, 2, Piece::Black); board.propegate_from(0, 2); assert_eq!(board.get(0, 1), &Some(Piece::Black)); } #[test] fn failed_capture() { let mut board = Board::new(); board.place_unchecked(0, 0, Piece::Black); board.place_unchecked(0, 2, Piece::White); board.place_unchecked(0, 3, Piece::Black); board.propegate_from(0, 3); assert_eq!( board.get(0, 1), &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, Piece::Black); for j in 1..=6 { board.place_unchecked(0, j, Piece::White); } board.place_unchecked(0, 7, Piece::Black); board.propegate_from(0, 7); for j in 2..=6 { assert_eq!( board.get(0, j), &Some(Piece::Black), "should be black at: ({}, {})", 0, j ); } } #[test] fn long_capture_vert() { let mut board = Board::new(); board.place_unchecked(0, 0, Piece::Black); for i in 1..=6 { board.place_unchecked(i, 0, Piece::White); } board.place_unchecked(7, 0, Piece::Black); board.propegate_from(7, 0); for i in 2..=6 { assert_eq!( board.get(i, 0), &Some(Piece::Black), "should be black at: ({}, {})", i, 0 ); } } #[test] fn diag_capture() { let mut board = Board::new().starting_pos(); assert_eq!(board.place(2, 4, Piece::White), Ok(()), "{}", board); assert_eq!(board.place(2, 3, Piece::Black), Ok(()), "{}", board); assert_eq!(board.place(2, 2, Piece::White), Ok(()), "{}", board); assert_eq!(board.place(2, 5, Piece::Black), Ok(()), "{}", board); } // Test corner capture from top-left corner #[test] fn corner_capture_top_left() { let mut board = Board::new(); // Black pieces at (0, 0) and (2, 2) board.place_unchecked(0, 0, Piece::Black); board.place_unchecked(1, 1, Piece::White); // to be captured board.place_unchecked(2, 2, Piece::Black); board.propegate_from(0, 0); // Capture white piece at (1,1) assert_eq!(board.get(1, 1), &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, Piece::Black); board.place_unchecked(1, 6, Piece::White); // to be captured board.place_unchecked(2, 5, Piece::Black); board.propegate_from(2, 5); // Capture white piece at (1, 6) assert_eq!(board.get(1, 6), &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, Piece::Black); board.place_unchecked(6, 1, Piece::White); // to be captured board.place_unchecked(5, 2, Piece::Black); board.propegate_from(5, 2); // Capture white piece at (6, 1) assert_eq!(board.get(6, 1), &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, Piece::Black); board.place_unchecked(6, 6, Piece::White); // to be captured board.place_unchecked(5, 5, Piece::Black); board.propegate_from(5, 5); // Capture white piece at (6, 6) assert_eq!(board.get(6, 6), &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, Piece::Black); board.place_unchecked(0, 1, Piece::White); // to be captured board.place_unchecked(0, 2, Piece::Black); board.propegate_from(0, 2); assert_eq!(board.get(0, 1), &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, Piece::Black); board.place_unchecked(0, 6, Piece::White); // to be captured board.place_unchecked(0, 5, Piece::Black); board.propegate_from(0, 5); assert_eq!(board.get(0, 6), &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, Piece::Black); board.place_unchecked(1, 0, Piece::White); // to be captured board.place_unchecked(2, 0, Piece::Black); board.propegate_from(2, 0); assert_eq!(board.get(1, 0), &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, Piece::Black); board.place_unchecked(6, 0, Piece::White); // to be captured board.place_unchecked(5, 0, Piece::Black); board.propegate_from(5, 0); assert_eq!(board.get(6, 0), &Some(Piece::Black), "\n{}", board); } }