diff --git a/othello/src/board.rs b/othello/src/board.rs index 5c1fa0c..fa3f28d 100644 --- a/othello/src/board.rs +++ b/othello/src/board.rs @@ -44,8 +44,8 @@ const DIRECTION_MASKS: [Board; 8] = [ /// Represents the board state using two integers. This allows us to reduce the /// memory footprint by 75% when compared against using a byte[8][8] (64 /// bytes). While digits in these boards do not have _place value_ per se, it -/// still may be helpful to think of the structure as being Little Endian and -/// by that I mean to say that a0 is the leftmost bit and h8 is the rightmost. +/// still may be helpful to think of the structure as being Big Endian and +/// by that I mean to say that h8 is the leftmost bit and a1 is the rightmost. /// /// The values are to be mapped as follows where each number indicates the bit /// in the integer that corresponds to the position in the board: @@ -70,6 +70,11 @@ const DIRECTION_MASKS: [Board; 8] = [ /// /// Note that I use the traditional nomenclature of ranks and files which /// correspond to rows and columns respectively. +/// +/// Like I note later on, there is some variation on how precisely ranks +/// and files are to be rendered or oriented due to the symmetrical nature +/// of Othello, but I stick to rigid Chess-esque conventions for the sake of my +/// sanity. pub struct BitBoard { /// Contains all boards for game. In this case, there are only two. /// The first is black. The second is white. @@ -78,9 +83,10 @@ pub struct BitBoard { impl Default for BitBoard { fn default() -> Self { + use squares::*; // Create board in standard starting structure Self { - boards: [(1 << 36) + (1 << 27), (1 << 35) + (1 << 28)], + boards: [D5 | E4, E5 | D4], } } } @@ -96,8 +102,19 @@ impl Debug for BitBoard { impl Display for BitBoard { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Render the board in a chess style. There seems to be differing opinions + // on how ranks and files should be displayed in Othello (mirroring vs rotation), + // so I just print the board in the Chess style since it makes more sense to me. + // + // For the sake of testing, it's also super convenient to not have the board + // flipping and rotating in the output. It makes it way easier to compare two + // boards side by side. + + // for each rank _r_ in reverse since our structure is Big Endian and we want + // to print the 8 rank first (chess style in white perspective). for r in (0..8).rev() { let start_i = r * 8; + write!(f, "{}: ", r + 1).unwrap(); for i in start_i..(start_i + 8) { // rshift to cut off bits on rhs, then AND on 1 to get just the last bit. write!( @@ -117,6 +134,7 @@ impl Display for BitBoard { write!(f, "\n").unwrap(); } } + write!(f, "\n abcdefgh").unwrap(); Ok(()) } } @@ -176,7 +194,7 @@ impl BitBoard { let mut moves = 0; let (team, opponent) = ( self.boards[current_team as usize], - self.boards[(current_team as usize + 1) % 2], + self.boards[current_team.next() as usize], ); // Instead of looping through a structure and checking adjacent @@ -222,15 +240,17 @@ impl BitBoard { // // Certain shifts are associated with particular masks by matching // indices. So for example, when we're looking to the right, we want to - // ignore the _h_ file. This is because there's no way of placing a piece - // in an orientation such that a piece in the _h_ file is flanked on its - // right side since the board ends. We do the same for when looking - // left with the _a_ file. + // ignore the _a_ file. This is because there's no way of placing a disc + // in an orientation such that a disc in the _a_ file is flanked on its + // right side. We do the same for when looking left with the _h_ file. for i in 0..8 { let shift = DIRECTIONS[i]; let mask = DIRECTION_MASKS[i]; // begin performing masks based on where opponent positions are + // since we can flank multiple pieces in a single direction, we + // apply this logic seven times as this would account for the + // longest possible flank. let mut sub_move = shift_in_direction(shift, team & mask) & opponent; sub_move |= shift_in_direction(shift, sub_move & mask) & opponent; sub_move |= shift_in_direction(shift, sub_move & mask) & opponent; @@ -327,224 +347,209 @@ mod tests { use super::*; #[test] - fn test_masking_logic() { - // validating the tests used in the comment explaining the way we're - // generating moves. + fn available_works() { let bb = BitBoard::default(); - let blk_shl = bb.boards[Team::Black as usize] << 1; - // validate that they are shifted to the right - assert_eq!(blk_shl, (1 << 37) + (1 << 28)); - // validate that they are shifted to the right visually - let expected_output = r"-------- --------- --------- ------B-- -----B--- --------- --------- ---------"; - assert_eq!( - format!( - "{}", - BitBoard { - boards: [blk_shl, 0] - } - ), - expected_output - ); - - // test that we can mask with white squares. - let matched = blk_shl & bb.boards[Team::White as usize]; - // validate that they are matching only - assert_eq!(matched, !Board::MAX + (1 << 28)); - // validate that they match visually - let expected_output = r"-------- --------- --------- --------- -----B--- --------- --------- ---------"; - assert_eq!( - format!( - "{}", - BitBoard { - boards: [matched, 0] - } - ), - expected_output - ); - } - - #[test] - fn avaliable_works() { - let bb = BitBoard::default(); - assert_eq!(bb.available(Team::Black), D6 + C5 + F4 + E3); + assert_eq!(bb.available(Team::Black), E6 | F5 | C4 | D3); } #[test] fn display_works() { let bb = BitBoard::default(); - let expected_output = r"-------- --------- --------- ----WB--- ----BW--- --------- --------- ---------"; + let expected_output = r"8: -------- +7: -------- +6: -------- +5: ---BW--- +4: ---WB--- +3: -------- +2: -------- +1: -------- + abcdefgh"; assert_eq!(format!("{}", bb), expected_output); } #[test] fn jon_works() { - let bb = BitBoard::from_jon("///3wb/3bw///").expect("Starting board should be valid"); + let bb = BitBoard::from_jon("///3bw/3wb///").expect("Starting board should be valid"); println!("{}", bb); assert_eq!(bb.boards, BitBoard::default().boards); } #[test] fn test_available_starting_position_black() { - // Starting position - let bb = BitBoard::from_jon("///3wb/3bw///").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black can move to: d3, c4, f5, e6 - let expected = BitBoard::from_jon("//4b/5b/2b/3b//").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); + let bb = BitBoard::default(); + assert_eq!(bb.available(Team::Black), E6 | F5 | C4 | D3); } #[test] fn test_available_starting_position_white() { - // Starting position - let bb = BitBoard::from_jon("///3wb/3bw///").expect("Valid board"); - let available = bb.available(Team::White); - - // White can move to: c5, d6, f4, e3 - let expected = BitBoard::from_jon("//3w/5w/2w/4w//").expect("Valid board"); - assert_eq!(available, expected.boards[Team::White as usize]); + let bb = BitBoard::default(); + assert_eq!(bb.available(Team::White), D6 | E3 | C5 | F4); } #[test] fn test_available_empty_board() { - // Empty board - no pieces - let bb = BitBoard::from_jon("////////").expect("Valid board"); - let available_black = bb.available(Team::Black); - let available_white = bb.available(Team::White); - - // No moves available on empty board - assert_eq!(available_black, 0); - assert_eq!(available_white, 0); + let bb = BitBoard::from_jon("///////").expect("Valid board"); + assert_eq!(bb.available(Team::Black), 0); + assert_eq!(bb.available(Team::White), 0); } #[test] fn test_available_single_piece_no_moves() { - // Single white piece, no black pieces - let bb = BitBoard::from_jon("///3w////").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black has no valid moves (needs opponent pieces to capture) - assert_eq!(available, 0); + // Only white at d4, no black pieces + let bb = BitBoard::from_jon("///3w///").expect("Valid board"); + assert_eq!(bb.available(Team::Black), 0); + assert_eq!(bb.available(Team::White), 0); } #[test] - fn test_available_corner_move() { - // Board with pieces set up for a corner capture - // Black at b8, white at a8, black can play at a7 to capture - let bb = BitBoard::from_jon("wb//////").expect("Valid board"); - println!("{}", bb); - let available = bb.available(Team::Black); - - // Black can move to a7 to capture white at a8 - let expected = BitBoard::from_jon("/b//////").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); - } - - #[test] - fn test_available_edge_captures() { - // Black pieces on edges with white pieces that can be captured - let bb = BitBoard::from_jon("b6w/8/8/8/8/8/8/8").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black can capture by playing in between - let expected = BitBoard::from_jon("1bbbbbb/").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); - } - - #[test] - fn test_available_multiple_direction_capture() { - // White surrounded by black - black can capture in multiple directions - let bb = BitBoard::from_jon("///2bbb/2bwb/2bbb///").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black can play at d4 (where white is) - but this is invalid, let me reconsider - // Actually, moves must be on empty squares - // Let's set up where black can capture in multiple directions - let bb = BitBoard::from_jon("///2b1b/2bwb/2bbb///").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black can move to d5 to capture white - let expected = BitBoard::from_jon("///3b////").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); - } - - #[test] - fn test_available_no_valid_moves() { - // Position where current team has no valid moves - // Black pieces isolated with no white pieces to capture - let bb = BitBoard::from_jon("b///////w").expect("Valid board"); - let available_black = bb.available(Team::Black); - let available_white = bb.available(Team::White); - - // Neither player can capture anything - assert_eq!(available_black, 0); - assert_eq!(available_white, 0); - } - - #[test] - fn test_available_full_row_capture() { - // Black can capture an entire row of white pieces - let bb = BitBoard::from_jon("///////bwwwwwwb").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black can play anywhere between the two black pieces on row 1 - let expected = BitBoard::from_jon("///////1bbbbbb").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); - } - - #[test] - fn test_available_diagonal_capture() { - // Test diagonal captures - let bb = BitBoard::from_jon("b/1w/2w/3w////").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black can capture diagonally - let expected = BitBoard::from_jon("////4b///").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); - } - - #[test] - fn test_available_midgame_position() { - // More complex mid-game position - let bb = BitBoard::from_jon("//2bwb/2www/2bwb///").expect("Valid board"); - let available = bb.available(Team::Black); - - // Black should have several valid moves - // This would need to be calculated based on actual game rules - // Adding moves that would flip white pieces - let expected = BitBoard::from_jon("/3b/b1b1b/b3b/b1b1b/3b//").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); + fn test_available_horizontal_line() { + // Black at a4, white at b4-f4, empty g4, black at h4 + let bb = BitBoard::from_jon("////bwwwww1b///").expect("Valid board"); + // Black can play at g4 to capture white pieces + assert_eq!(bb.available(Team::Black), G4); } #[test] fn test_available_vertical_capture() { - // Test vertical captures - let bb = BitBoard::from_jon("b/w/w/w////").expect("Valid board"); - let available = bb.available(Team::Black); + // Black at a8, white at a7, a6, a5, empty a4, black at a3 + let bb = BitBoard::from_jon("b/w/w/w//b//").expect("Valid board"); + // Black can play at a4 to capture white pieces between a3 and a8 + assert_eq!(bb.available(Team::Black), A4); + } - // Black can capture vertically downward - let expected = BitBoard::from_jon("////b///").expect("Valid board"); - assert_eq!(available, expected.boards[Team::Black as usize]); + #[test] + fn test_available_diagonal_capture_tr() { + // Black at a1, white at b2, c3, empty d4 + let bb = BitBoard::from_jon("/////2w/1w/b").expect("Valid board"); + // Black can play at d4 to reverse on top-right diagonal + assert_eq!(bb.available(Team::Black), D4); + } + + #[test] + fn test_available_diagonal_capture_tl() { + // Black at h1, white at g2, f3, empty e4 + let bb = BitBoard::from_jon("/////5w/6w/7b").expect("Valid board"); + // Black can play at e4 to reverse on top-left-diagonal + assert_eq!(bb.available(Team::Black), E4); + } + + #[test] + fn test_available_diagonal_capture_br() { + // Black at a1, white at b2, c3, empty d4 + let bb = BitBoard::from_jon("w/1b/2b").expect("Valid board"); + // White can play at d5 to reverse on bottom-right diagonal + assert_eq!(bb.available(Team::White), D5); + } + + #[test] + fn test_available_diagonal_capture_bl() { + // Black at h1, white at g2, f3, empty e4 + let bb = BitBoard::from_jon("7w/6b/5b").expect("Valid board"); + // White can play at e5 to reverse on bottom-left diagonal + assert_eq!(bb.available(Team::White), E5); + } + + #[test] + fn test_available_corner_a1() { + // Setup where black can capture into corner a1 + // White at b1, black at c1, empty a1 + let bb = BitBoard::from_jon("///////wb").expect("Valid board"); + // Black can play at a1 to capture b1 + assert_eq!(bb.available(Team::Black), 0); + assert_eq!(bb.available(Team::White), C1); + } + + #[test] + fn test_available_corner_h8() { + // Setup where black can capture into corner h8 + // Black at f8, white at g8, empty h8 + let bb = BitBoard::from_jon("5bw//////").expect("Valid board"); + // Black can play at h8 to capture g8 + assert_eq!(bb.available(Team::Black), H8); + } + + #[test] + fn test_available_multiple_directions() { + // White at d4, black at d3, d5, c4, e4 (surrounding white) + let bb = BitBoard::from_jon("///3b/2bwb/3b//").expect("Valid board"); + // White can play at d2, d6, b4, or f4 to capture black + assert_eq!(bb.available(Team::White), D2 | D6 | B4 | F4); + } + + #[test] + fn test_available_no_moves_surrounded() { + // Black piece at b7 completely surrounded by white + let bb = BitBoard::from_jon("www/wbw/www////").expect("Valid board"); + println!("dis:\n{}", bb); + // Black should have three moves in this position: d7, b5, and d5 + assert_eq!(bb.available(Team::Black), D7 | B5 | D5); + } + + #[test] + fn test_available_long_capture() { + // Black at a4, white at b4-f4, empty g4, black at h4 + let bb = BitBoard::from_jon("////bwwwww1b///").expect("Valid board"); + println!("dis:\n{}", bb); + // Black can play at g4 to capture entire row of white pieces + assert_eq!(bb.available(Team::Black), G4); + } + + #[test] + fn test_available_edge_moves() { + // Black at a1, white at b1, c1, empty d1 + let bb = BitBoard::from_jon("///////bww").expect("Valid board"); + // Black can play at d1 to capture b1 and c1 + assert_eq!(bb.available(Team::Black), D1); + } + + #[test] + fn test_available_after_first_move() { + // After black plays d3 from starting position + let bb = BitBoard::from_jon("///3bw/2bbw/3b//").expect("Valid board"); + // White should be able to play at several positions + let available = bb.available(Team::White); + // White can at minimum play c3, e3, c5 + assert_ne!(available, 0); + assert_eq!(available & C3, C3); + } + + #[test] + fn test_available_complex_midgame() { + // A complex position with multiple pieces + let bb = BitBoard::from_jon("//1b2w/1bwwww/1bwbww/1bwwww/1b3/").expect("Valid board"); + let available = bb.available(Team::Black); + // Black should have at least some moves available + assert_ne!(available, 0); + // In fact, it should have the following options available: + assert_eq!(available, F7 | D6 | F6 | G4 | F2 | E2 | D2 | G5 | G3); + } + + #[test] + fn test_available_no_opponent_pieces() { + // Board with only white pieces (two rows) + let bb = BitBoard::from_jon("wwwwwwww/wwwwwwww/////").expect("Valid board"); + // Black has no pieces, so no moves + assert_eq!(bb.available(Team::Black), 0); + assert_eq!(bb.available(Team::White), 0); + } + + #[test] + fn test_available_all_four_corners() { + // Test capture opportunities in all four corners + // a1 corner: white at b1, black at c1 + let bb = BitBoard::from_jon("///////wb").expect("Valid board"); + assert_eq!(bb.available(Team::White), C1); + + // h1 corner: black at f1, white at g1 + let bb = BitBoard::from_jon("///////6bw").expect("Valid board"); + assert_eq!(bb.available(Team::White), F1); + + // a8 corner: white at b8, black at c8 + let bb = BitBoard::from_jon("wb").expect("Valid board"); + assert_eq!(bb.available(Team::White), C8); + + // h8 corner: white at b8, black at c8 + let bb = BitBoard::from_jon("6wb").expect("Valid board"); + assert_eq!(bb.available(Team::Black), F8); } } diff --git a/othello/src/game.rs b/othello/src/game.rs index a960ff5..7d82aa7 100644 --- a/othello/src/game.rs +++ b/othello/src/game.rs @@ -4,3 +4,14 @@ pub enum Team { Black, White, } + +impl Team { + /// Just return the other team or the next team. + /// This is useful for modeling state transfer + pub fn next(&self) -> Self { + match self { + Team::Black => Team::White, + Team::White => Team::Black, + } + } +}