restructured, flips working, benchmarking flips, got some basic terminal rendering
This commit is contained in:
parent
10de749e1d
commit
d7d8732904
17 changed files with 331 additions and 822 deletions
710
src/board/mod.rs
Normal file
710
src/board/mod.rs
Normal file
|
|
@ -0,0 +1,710 @@
|
|||
//! 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::{
|
||||
board::view::{Overlay, View},
|
||||
game::Team,
|
||||
};
|
||||
|
||||
pub mod view;
|
||||
|
||||
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
|
||||
// simply negating the A file
|
||||
const NOT_A_FILE: Board = 0x7F7F7F7F7F7F7F7F;
|
||||
// simply negating the H file
|
||||
const NOT_H_FILE: Board = 0xFEFEFEFEFEFEFEFE;
|
||||
const SHIFT_MASK_COMBOS: [(i8, Board); 8] = [
|
||||
(9, NOT_A_FILE), // 9 (up right)
|
||||
(8, Board::MAX), // 8 (up)
|
||||
(7, NOT_H_FILE), // 7 (up left)
|
||||
(1, NOT_A_FILE), // 1 (right)
|
||||
(-1, NOT_H_FILE), // -1 (left)
|
||||
(-7, NOT_A_FILE), // -7 (down right)
|
||||
(-8, Board::MAX), // -8 (down)
|
||||
(-9, 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 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:
|
||||
///
|
||||
/// ```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.
|
||||
///
|
||||
/// 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.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub struct BitBoard {
|
||||
/// Contains all boards for game. In this case, there are only two.
|
||||
/// The first is black. The second is white.
|
||||
pub boards: [Board; 2],
|
||||
}
|
||||
|
||||
impl Default for BitBoard {
|
||||
fn default() -> Self {
|
||||
use squares::*;
|
||||
// Create board in standard starting structure
|
||||
Self {
|
||||
boards: [D5 | E4, E5 | D4],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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!(
|
||||
f,
|
||||
"{}",
|
||||
if (self.boards[0] >> i) & 1 == 1 {
|
||||
'B'
|
||||
} else if (self.boards[1] >> i) & 1 == 1 {
|
||||
'W'
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if r != 0 {
|
||||
writeln!(f).unwrap();
|
||||
}
|
||||
}
|
||||
write!(f, "\n abcdefgh").unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn shift_in_direction(magnitude: i8, value: Board) -> Board {
|
||||
if magnitude >= 0 {
|
||||
let (result, _) = value.overflowing_shl(magnitude.unsigned_abs() as u32);
|
||||
result
|
||||
} else {
|
||||
let (result, _) = value.overflowing_shr(magnitude.unsigned_abs() as u32);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl BitBoard {
|
||||
/// Create a new BitBoard from provided team boards.
|
||||
pub fn new(black: Board, white: Board) -> Self {
|
||||
Self {
|
||||
boards: [black, white],
|
||||
}
|
||||
}
|
||||
/// Convert from a JON (Jack Othello Notation) string into a usable board.
|
||||
/// Format:
|
||||
///
|
||||
/// | Symbol or Pattern | Meaning |
|
||||
/// | ------------------ | --------------------------------- |
|
||||
/// | `/` | _next line_ |
|
||||
/// | `[0-9]` | _jump squares_ |
|
||||
/// | `[wWbB]` | _white or black disc at position_ |
|
||||
///
|
||||
/// Example: `///3bw/3wb///` is the JON for the starting position.
|
||||
/// It means three empty ranks, three empty spaces followed by a black disc,
|
||||
/// a white disc, next rank, three empty spaces followed by a white disc,
|
||||
/// a black disc, and then three empty ranks. It also technically could be
|
||||
/// truncated after the second `b` as `///3bw/3wb` since trailing rank is
|
||||
/// optional.
|
||||
pub fn from_jon(fen: &str) -> Result<Self, String> {
|
||||
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() {
|
||||
match c.to_ascii_lowercase() {
|
||||
'/' => {
|
||||
line = line.checked_sub(1).ok_or("Malformed JON: Too many ranks")?;
|
||||
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'..='8' => {
|
||||
rank_jump = c
|
||||
.to_digit(10u32)
|
||||
.ok_or("JON Parsing error: Failed to parse integer from character")?
|
||||
as u8;
|
||||
}
|
||||
c => {
|
||||
return Err(format!(
|
||||
"Malformed JON: Unexpected character encountered ({})",
|
||||
c
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self { boards })
|
||||
}
|
||||
|
||||
pub fn player_at(&self, rank: u8, file: u8) -> Option<Team> {
|
||||
if self.boards[0] & (1 << (rank * 8 + file)) != 0 {
|
||||
Some(Team::Black)
|
||||
} else if self.boards[1] & (1 << (rank * 8 + file)) != 0 {
|
||||
Some(Team::White)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Compute board with valid moves marked using only bitwise operations.
|
||||
/// This has constant time complexity and is unbelievably fast.
|
||||
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.next() as usize],
|
||||
);
|
||||
|
||||
// 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 _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 (shift, mask) in SHIFT_MASK_COMBOS {
|
||||
// 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;
|
||||
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
|
||||
}
|
||||
|
||||
pub fn render(&self, view: impl Into<View>, overlays: Vec<Overlay>) -> String {
|
||||
let view: View = view.into();
|
||||
let mut result = String::new();
|
||||
|
||||
let raw_r_range = 0..8;
|
||||
let r_range = match view {
|
||||
View::RankAsc => Box::new(raw_r_range) as Box<dyn Iterator<Item = i32>>,
|
||||
View::RankDesc => Box::new(raw_r_range.rev()) as Box<dyn Iterator<Item = i32>>,
|
||||
};
|
||||
|
||||
for (r_idx, r) in r_range.enumerate() {
|
||||
let start_i = r * 8;
|
||||
result.push_str(&format!("{}: ", r + 1));
|
||||
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.
|
||||
result.push_str(&format!(
|
||||
"\x1b[42m{} \x1b[0m",
|
||||
if (self.boards[0] >> i) & 1 == 1 {
|
||||
String::from("\x1b[30m●")
|
||||
} else if (self.boards[1] >> i) & 1 == 1 {
|
||||
String::from("\x1b[97m●")
|
||||
} else {
|
||||
// if our overlay provides a character for the square, use it.
|
||||
// otherwise use the default char '-'
|
||||
overlays
|
||||
.iter()
|
||||
.find(|Overlay(board, _)| (board >> i) & 1 == 1)
|
||||
.map(|Overlay(_, c)| *c)
|
||||
.unwrap_or("░")
|
||||
.to_owned()
|
||||
}
|
||||
));
|
||||
}
|
||||
if r_idx != 7 {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
result.push_str("\n a b c d e f g h");
|
||||
|
||||
result
|
||||
}
|
||||
/// Apply play to a board and compute effected reversals
|
||||
pub fn play(&mut self, current_team: Team, play: Board) {
|
||||
// bitwise OR gives spots with either white OR black discs
|
||||
// bitwise NEG gives the spots with neither white nor black discs
|
||||
let mut flips = 0;
|
||||
let current_team_idx = current_team as usize;
|
||||
let (team, opponent) = (
|
||||
self.boards[current_team_idx],
|
||||
self.boards[current_team.next() as usize],
|
||||
);
|
||||
|
||||
// For each (shift, mask) pair, check whether there's an opponent disc in
|
||||
// the direction of the shift. We mask with `mask` before shifting to avoid
|
||||
// wrapping horizontally around the edges of the board. We do this six times
|
||||
// since the maximum amount of flipped discs is six. After we're finished
|
||||
// shifting, we shift once more with the same rules and then check if we
|
||||
// intersect with a disc of our own by comparing the bitwise conjunction to
|
||||
// zero. If it's greater than zero, then we've definitely detected a() flip(s)
|
||||
// in that direction. We then add these to our running summation of flips.
|
||||
|
||||
for (shift, mask) in SHIFT_MASK_COMBOS {
|
||||
let mut flip = shift_in_direction(shift, play & mask) & opponent;
|
||||
flip |= shift_in_direction(shift, flip & mask) & opponent;
|
||||
flip |= shift_in_direction(shift, flip & mask) & opponent;
|
||||
flip |= shift_in_direction(shift, flip & mask) & opponent;
|
||||
flip |= shift_in_direction(shift, flip & mask) & opponent;
|
||||
flip |= shift_in_direction(shift, flip & mask) & opponent;
|
||||
if shift_in_direction(shift, flip & mask) & team > 0 {
|
||||
flips |= flip;
|
||||
}
|
||||
}
|
||||
|
||||
self.boards[current_team_idx] += flips;
|
||||
self.boards[current_team.next() as usize] ^= flips;
|
||||
|
||||
self.boards[current_team_idx] += play;
|
||||
}
|
||||
|
||||
/// Compute the score (B, W) by counting the excited bits in each board.
|
||||
pub fn score(&self) -> (u32, u32) {
|
||||
// `u64::count_ones()` is implemented `unsafe` and makes use of primitives in the
|
||||
// low-level implementation of the language itself for maximum performance. Better
|
||||
// to use this than implement myself.
|
||||
(self.boards[0].count_ones(), self.boards[1].count_ones())
|
||||
}
|
||||
}
|
||||
|
||||
pub 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 available_works() {
|
||||
let bb = BitBoard::default();
|
||||
assert_eq!(bb.available(Team::Black), E6 | F5 | C4 | D3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_works() {
|
||||
let bb = BitBoard::default();
|
||||
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("///3bw/3wb///").expect("Starting board should be valid");
|
||||
assert_eq!(bb.boards, BitBoard::default().boards);
|
||||
// trailing slashes are optional
|
||||
let bb = BitBoard::from_jon("///3bw/3wb")
|
||||
.expect("Starting board without trailing slashes should be valid");
|
||||
assert_eq!(bb.boards, BitBoard::default().boards);
|
||||
// test with too many slashes
|
||||
assert!(BitBoard::from_jon("////////").is_err());
|
||||
// test with unexpected char
|
||||
assert!(BitBoard::from_jon("//c/////").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_starting_position_black() {
|
||||
let bb = BitBoard::default();
|
||||
assert_eq!(bb.available(Team::Black), E6 | F5 | C4 | D3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_starting_position_white() {
|
||||
let bb = BitBoard::default();
|
||||
assert_eq!(bb.available(Team::White), D6 | E3 | C5 | F4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_available_empty_board() {
|
||||
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() {
|
||||
// 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_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() {
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn render_dbg(board: BitBoard) -> BitBoard {
|
||||
println!("dbg:\n{}", board.render(View::RankAsc, vec![]));
|
||||
board
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_works() {
|
||||
let mut bb = BitBoard::default();
|
||||
assert_eq!(bb.score(), (2, 2));
|
||||
bb.play(Team::Black, E6);
|
||||
assert_eq!(bb, BitBoard::new(D5 | E6 | E5 | E4, D4));
|
||||
assert_eq!(bb.score(), (4, 1));
|
||||
bb.play(Team::White, F4);
|
||||
assert_eq!(bb, BitBoard::new(D5 | E6 | E5, D4 | E4 | F4));
|
||||
assert_eq!(bb.score(), (3, 3));
|
||||
bb.play(Team::Black, G3);
|
||||
assert_eq!(bb, BitBoard::new(D5 | E6 | E5 | G3 | F4, D4 | E4));
|
||||
assert_eq!(bb.score(), (5, 2));
|
||||
bb.play(Team::White, E7);
|
||||
assert_eq!(bb, BitBoard::new(D5 | G3 | F4, D4 | E4 | E5 | E6 | E7));
|
||||
assert_eq!(bb.score(), (3, 5));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue