550 lines
18 KiB
Rust
550 lines
18 KiB
Rust
//! All things to do with the Othello game board. This module contains types,
|
|
//! traits, and functions for working with the board.
|
|
|
|
use std::fmt::{Debug, Display};
|
|
|
|
use crate::game::Team;
|
|
|
|
pub type Board = u64;
|
|
|
|
// Directions:
|
|
// 7 8 9
|
|
// -1 0 1
|
|
// -9 -8 -7
|
|
//
|
|
// 0 is where "we" are. The other numbers represent the index shifts to access
|
|
// both orthogonal and diagonal adjecent squares.
|
|
//
|
|
// Note that negative numbers indicate a shift to the right while positive ones
|
|
// indicate a left shift.
|
|
//
|
|
// When we want to shift from a higher place to a lower place (when looking
|
|
// straight to the right or up in any direction for example), we aren't really
|
|
// moving toward _it_. It's more so that we're moving that toward us. So a
|
|
// digit further along in the structure (positive numbers) will require
|
|
// shifting to the left to bring it to match our current focus. Thus:
|
|
// Positive => SHL
|
|
// Negative => SHR
|
|
const DIRECTIONS: [i8; 8] = [9, 8, 7, 1, -1, -7, -8, -9];
|
|
// simply negating the A file
|
|
const NOT_A_FILE: Board = 0x7F7F7F7F7F7F7F7F;
|
|
// simply negating the H file
|
|
const NOT_H_FILE: Board = 0xFEFEFEFEFEFEFEFE;
|
|
const DIRECTION_MASKS: [Board; 8] = [
|
|
NOT_A_FILE, // 9 (up right)
|
|
Board::MAX, // 8 (up)
|
|
NOT_H_FILE, // 7 (up left)
|
|
NOT_A_FILE, // 1 (right)
|
|
NOT_H_FILE, // -1 (left)
|
|
NOT_A_FILE, // -7 (down right)
|
|
Board::MAX, // -8 (down)
|
|
NOT_H_FILE, // -9 (down left)
|
|
];
|
|
|
|
/// 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.
|
|
///
|
|
/// 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:
|
|
///
|
|
/// ```text
|
|
/// a: b: c: d: e: f: g: h:
|
|
/// 8: 56 57 58 59 60 61 62 63 :8
|
|
/// 7: 48 49 50 51 52 53 54 55 :7
|
|
/// 6: 40 41 42 43 44 45 46 47 :6
|
|
/// 5: 32 33 34 35 36 37 38 39 :5
|
|
/// 4: 24 25 26 27 28 29 30 31 :4
|
|
/// 3: 16 17 18 19 20 21 22 23 :3
|
|
/// 2: 08 09 10 11 12 13 14 15 :2
|
|
/// 1: 00 01 02 03 04 05 06 07 :1
|
|
/// a: b: c: d: e: f: g: h:
|
|
///
|
|
/// =
|
|
///
|
|
/// bits: 00 01 ... 07 08 09 ... 15 ... 56 57 ... 63
|
|
/// board: a1 b1 ... h1 a2 b2 ... h2 ... a8 b8 ... h8
|
|
/// ```
|
|
///
|
|
/// Note that I use the traditional nomenclature of ranks and files which
|
|
/// correspond to rows and columns respectively.
|
|
pub struct BitBoard {
|
|
/// Contains all boards for game. In this case, there are only two.
|
|
/// The first is black. The second is white.
|
|
boards: [Board; 2],
|
|
}
|
|
|
|
impl Default for BitBoard {
|
|
fn default() -> Self {
|
|
// Create board in standard starting structure
|
|
Self {
|
|
boards: [(1 << 36) + (1 << 27), (1 << 35) + (1 << 28)],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for BitBoard {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("BitBoard")
|
|
.field("black", &format!("0x{:X}", &self.boards[0]))
|
|
.field("white", &format!("0x{:X}", &self.boards[1]))
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl Display for BitBoard {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
for r in (0..8).rev() {
|
|
let start_i = r * 8;
|
|
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!(
|
|
f,
|
|
"{}",
|
|
if (self.boards[0] >> i) & 1 == 1 {
|
|
'B'
|
|
} else if (self.boards[1] >> i) & 1 == 1 {
|
|
'W'
|
|
} else {
|
|
'-'
|
|
}
|
|
)
|
|
.unwrap();
|
|
}
|
|
if r != 0 {
|
|
write!(f, "\n").unwrap();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn shift_in_direction(magnitude: i8, value: Board) -> Board {
|
|
if magnitude >= 0 {
|
|
let (result, _) = value.overflowing_shl(magnitude.abs() as u32);
|
|
result
|
|
} else {
|
|
let (result, _) = value.overflowing_shr(magnitude.abs() as u32);
|
|
result
|
|
}
|
|
}
|
|
|
|
impl BitBoard {
|
|
/// Convert from a JON (Jack Othello Notation) string into a usable board.
|
|
pub fn from_jon(fen: &str) -> Result<Self, &'static str> {
|
|
let mut boards: [Board; 2] = [0, 0];
|
|
let mut line: u8 = 7;
|
|
let mut rank_idx: u8 = 0;
|
|
let mut rank_jump: u8 = 0;
|
|
for c in fen.chars().into_iter() {
|
|
match c.to_ascii_lowercase() {
|
|
'/' => {
|
|
line -= 1;
|
|
rank_idx = 0;
|
|
rank_jump = 0;
|
|
}
|
|
'b' => {
|
|
boards[0] += 1 << (line * 8 + rank_idx + rank_jump);
|
|
rank_idx += rank_jump + 1;
|
|
rank_jump = 0;
|
|
}
|
|
'w' => {
|
|
boards[1] += 1 << (line * 8 + rank_idx + rank_jump);
|
|
rank_idx += rank_jump + 1;
|
|
rank_jump = 0;
|
|
}
|
|
'0'..'9' => {
|
|
rank_jump = c
|
|
.to_digit(10u32)
|
|
.ok_or("Failed to parse integer from character")?
|
|
as u8;
|
|
}
|
|
_ => {
|
|
return Err("Malformed FEN: Unexpected character encountered");
|
|
}
|
|
}
|
|
}
|
|
Ok(Self { boards })
|
|
}
|
|
/// Compute board with valid moves marked
|
|
pub fn available(&self, current_team: Team) -> Board {
|
|
// bitwise OR gives spots with either white OR black discs
|
|
// bitwise NEG gives the spots with neither white nor black discs
|
|
let empties = !(self.boards[0] | self.boards[1]);
|
|
let mut moves = 0;
|
|
let (team, opponent) = (
|
|
self.boards[current_team as usize],
|
|
self.boards[(current_team as usize + 1) % 2],
|
|
);
|
|
|
|
// Instead of looping through a structure and checking adjacent
|
|
// squares, we are able to rather easily aggregate these operations
|
|
// into a handful of bitwise operations. So for example, let's
|
|
// consider the starting board (illustrations omitting some
|
|
// unimportant ranks):
|
|
//
|
|
// Game Black White
|
|
// -------- -------- --------
|
|
// ---WB--- = ----B--- + ---W----
|
|
// ---BW--- ---B---- ----W---
|
|
// -------- -------- --------
|
|
//
|
|
// It's black's turn. Let's first consider how we might find any
|
|
// white pieces that are adjacent to black pieces. Let's just check
|
|
// for white pieces that are to the _right_ of black ones. We can do
|
|
// this by shifting all black digits to the left once (see info about
|
|
// shifts and directions earlier in module).
|
|
//
|
|
// Black << 1 =
|
|
// --------
|
|
// -----B--
|
|
// ----B---
|
|
// --------
|
|
//
|
|
// We can then find where this shifted board intersects with white's
|
|
// board using bitwise AND.
|
|
//
|
|
// Black<<1 & White = Right-adjacents (A)
|
|
//
|
|
// -------- -------- --------
|
|
// -----B-- & ---W---- = --------
|
|
// ----B--- ----W--- ----A---
|
|
// -------- -------- --------
|
|
//
|
|
// We now have all the white matrices to the right of black matrices.
|
|
// We can continue doing this in different directions using different
|
|
// values to shift with (they are the same every time), using masks
|
|
// here and there to avoid problems with the outer files, and then
|
|
// bitwise OR-ing these together to get a matrix containing all
|
|
// adjacencies.
|
|
//
|
|
// 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.
|
|
|
|
for i in 0..8 {
|
|
let shift = DIRECTIONS[i];
|
|
let mask = DIRECTION_MASKS[i];
|
|
// begin performing masks based on where opponent positions are
|
|
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;
|
|
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
|
|
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
|
|
sub_move |= shift_in_direction(shift, sub_move & mask) & opponent;
|
|
sub_move = shift_in_direction(shift, sub_move & mask) & empties;
|
|
moves |= sub_move;
|
|
}
|
|
|
|
moves
|
|
}
|
|
}
|
|
|
|
mod squares {
|
|
/// Just allows us to easily define squares in terms of their shift
|
|
macro_rules! bitboard_consts {
|
|
($($name:ident = $digit:expr), * $(,)?) => {
|
|
$(#[allow(dead_code)] pub const $name: super::Board = 1 << $digit;)*
|
|
};
|
|
}
|
|
|
|
bitboard_consts!(
|
|
A1 = 0,
|
|
B1 = 1,
|
|
C1 = 2,
|
|
D1 = 3,
|
|
E1 = 4,
|
|
F1 = 5,
|
|
G1 = 6,
|
|
H1 = 7,
|
|
A2 = 8,
|
|
B2 = 9,
|
|
C2 = 10,
|
|
D2 = 11,
|
|
E2 = 12,
|
|
F2 = 13,
|
|
G2 = 14,
|
|
H2 = 15,
|
|
A3 = 16,
|
|
B3 = 17,
|
|
C3 = 18,
|
|
D3 = 19,
|
|
E3 = 20,
|
|
F3 = 21,
|
|
G3 = 22,
|
|
H3 = 23,
|
|
A4 = 24,
|
|
B4 = 25,
|
|
C4 = 26,
|
|
D4 = 27,
|
|
E4 = 28,
|
|
F4 = 29,
|
|
G4 = 30,
|
|
H4 = 31,
|
|
A5 = 32,
|
|
B5 = 33,
|
|
C5 = 34,
|
|
D5 = 35,
|
|
E5 = 36,
|
|
F5 = 37,
|
|
G5 = 38,
|
|
H5 = 39,
|
|
A6 = 40,
|
|
B6 = 41,
|
|
C6 = 42,
|
|
D6 = 43,
|
|
E6 = 44,
|
|
F6 = 45,
|
|
G6 = 46,
|
|
H6 = 47,
|
|
A7 = 48,
|
|
B7 = 49,
|
|
C7 = 50,
|
|
D7 = 51,
|
|
E7 = 52,
|
|
F7 = 53,
|
|
G7 = 54,
|
|
H7 = 55,
|
|
A8 = 56,
|
|
B8 = 57,
|
|
C8 = 58,
|
|
D8 = 59,
|
|
E8 = 60,
|
|
F8 = 61,
|
|
G8 = 62,
|
|
H8 = 63,
|
|
);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::squares::*;
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_masking_logic() {
|
|
// validating the tests used in the comment explaining the way we're
|
|
// generating moves.
|
|
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);
|
|
}
|
|
|
|
#[test]
|
|
fn display_works() {
|
|
let bb = BitBoard::default();
|
|
let expected_output = r"--------
|
|
--------
|
|
--------
|
|
---WB---
|
|
---BW---
|
|
--------
|
|
--------
|
|
--------";
|
|
assert_eq!(format!("{}", bb), expected_output);
|
|
}
|
|
|
|
#[test]
|
|
fn jon_works() {
|
|
let bb = BitBoard::from_jon("///3wb/3bw///").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]);
|
|
}
|
|
|
|
#[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]);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[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]);
|
|
}
|
|
|
|
#[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 can capture vertically downward
|
|
let expected = BitBoard::from_jon("////b///").expect("Valid board");
|
|
assert_eq!(available, expected.boards[Team::Black as usize]);
|
|
}
|
|
}
|