fix: bitboard not reflecting captures

also implemented perft
This commit is contained in:
dogeystamp 2024-10-22 16:31:30 -04:00
parent 8b066b3933
commit 61e5a6114b
3 changed files with 157 additions and 21 deletions

19
contrib/bitboard_print.py Normal file
View File

@ -0,0 +1,19 @@
"""
Print bitboard integer.
Usage:
python3 contrib/bitboard_print.py 71213177697730560
"""
from sys import argv
def chunks(s: str, n: int = 8):
for i in range(len(s) // n):
yield s[i * n : (i + 1) * n]
val = bin(int(argv[1]))[2:].zfill(64)
print("\n".join(''.join(reversed(c)) for c in chunks(val)))

View File

@ -110,7 +110,7 @@ impl ColPiece {
/// Square index newtype.
///
/// A1 is (0, 0) -> 0, A2 is (0, 1) -> 2, and H8 is (7, 7) -> 63.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Square(usize);
#[derive(Debug)]
@ -269,16 +269,21 @@ impl From<Piece> for char {
struct Bitboard(u64);
impl Bitboard {
pub fn on_idx(&mut self, idx: Square) {
//! Set the square at an index to on.
pub fn on_sq(&mut self, idx: Square) {
//! Set a square on.
self.0 |= 1 << usize::from(idx);
}
pub fn off_idx(&mut self, idx: Square) {
//! Set the square at an index to off.
pub fn off_sq(&mut self, idx: Square) {
//! Set a square off.
self.0 &= !(1 << usize::from(idx));
}
pub fn get_sq(&self, idx: Square) -> bool {
//! Read the value at a square.
(self.0 & 1 << usize::from(idx)) == 1
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
@ -307,7 +312,7 @@ impl Iterator for BitboardIterator {
} else {
let next_idx = self.remaining.0.trailing_zeros() as usize;
let sq = Square(next_idx);
self.remaining.off_idx(sq);
self.remaining.off_sq(sq);
Some(sq)
}
}
@ -453,8 +458,9 @@ impl BoardState {
/// Create a new piece in a location.
fn set_piece(&mut self, idx: Square, pc: ColPiece) {
let _ = self.del_piece(idx);
let pl = self.pl_mut(pc.col);
pl.board_mut(pc.into()).on_idx(idx);
pl.board_mut(pc.into()).on_sq(idx);
*self.mail.sq_mut(idx) = Some(pc);
}
@ -474,7 +480,7 @@ impl BoardState {
fn del_piece(&mut self, idx: Square) -> Result<ColPiece, NoPieceError> {
if let Some(pc) = *self.mail.sq_mut(idx) {
let pl = self.pl_mut(pc.col);
pl.board_mut(pc.into()).off_idx(idx);
pl.board_mut(pc.into()).off_sq(idx);
*self.mail.sq_mut(idx) = None;
Ok(pc)
} else {
@ -604,7 +610,7 @@ mod tests {
let squares = indices.map(Square);
for sq in squares {
bitboard.on_idx(sq);
bitboard.on_sq(sq);
}
// ensure that iteration does not consume the board
for _ in 0..=1 {

View File

@ -1,6 +1,6 @@
//! Move generation.
use crate::fen::{FromFen, START_POSITION};
use crate::fen::{FromFen, ToFen, START_POSITION};
use crate::{
BoardState, ColPiece, Color, Piece, Square, SquareError, BOARD_HEIGHT, BOARD_WIDTH, N_SQUARES,
};
@ -30,9 +30,11 @@ impl Node {
/// Intended usage is to always keep an Rc to the current node, and overwrite it with the
/// result of unmake.
pub fn unmake(&self) -> Rc<Node> {
self.prev
.clone()
.expect("unmake should not be called on root node")
if let Some(prev) = &self.prev {
Rc::clone(prev)
} else {
panic!("unmake should not be called on root node");
}
}
}
@ -94,16 +96,23 @@ impl Move {
}
/// Perform sanity checks.
macro_rules! pc_asserts {
($pc_src: ident, $data: ident) => {
debug_assert_eq!($pc_src.col, new_pos.turn, "Moving piece on wrong turn.");
debug_assert_ne!($data.src, $data.dest, "Moving piece to itself.");
($pc_src: ident) => {
debug_assert_eq!(
$pc_src.col,
new_pos.turn,
"Moving piece on wrong turn. Move {} -> {} on board '{}'",
self.src,
self.dest,
old_pos.to_fen()
);
debug_assert_ne!(self.src, self.dest, "Moving piece to itself.");
};
}
match self.move_type {
MoveType::Promotion(to_piece) => {
let pc_src = pc_src!(self);
pc_asserts!(pc_src, self);
pc_asserts!(pc_src);
debug_assert_eq!(pc_src.pc, Piece::Pawn);
let _ = new_pos.del_piece(self.src);
@ -117,7 +126,7 @@ impl Move {
}
MoveType::Normal => {
let pc_src = pc_src!(self);
pc_asserts!(pc_src, self);
pc_asserts!(pc_src);
let pc_dest: Option<ColPiece> = new_pos.get_piece(self.dest);
@ -229,12 +238,13 @@ impl Move {
///
/// Old position is saved in a backlink.
/// No checking is done to verify even pseudo-legality of the move.
pub fn make(self, old_node: Rc<Node>) -> Node {
pub fn make(self, old_node: &Rc<Node>) -> Rc<Node> {
let pos = self.make_unlinked(old_node.pos);
Node {
prev: Some(old_node),
prev: Some(Rc::clone(old_node)),
pos,
}
.into()
}
}
@ -407,6 +417,7 @@ impl PseudoMoveGen for BoardState {
pl.board(Piece::$pc).into_iter()
};
}
for sq in squares!(Rook) {
move_slider(self, sq, &mut ret, SliderDirection::Straight, true);
}
@ -658,11 +669,51 @@ impl LegalMoveGen for Node {
})
}
}
/// How many nodes at depth N can be reached from this position.
fn perft(depth: usize, node: &Rc<Node>) -> usize {
if depth == 0 {
return 1;
};
let mut ans = 0;
let moves = node.gen_moves();
for mv in moves {
let new_node = mv.make(node);
ans += perft(depth - 1, &new_node);
}
ans
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fen::{ToFen, START_POSITION};
#[test]
/// Ensure that bitboard properly reflects captures.
fn test_bitboard_capture() {
let board = BoardState::from_fen("8/8/8/8/8/8/r7/R7 w - - 0 1").unwrap();
let mv = Move::from_uci_algebraic("a1a2").unwrap();
// there's an assertion within make move that should
let new_pos = mv.make_unlinked(board);
use std::collections::hash_set::HashSet;
use Piece::*;
for pc in [Rook, Bishop, Knight, Queen, King, Pawn] {
let white: HashSet<_> = new_pos.pl(Color::White).board(pc).into_iter().collect();
let black: HashSet<_> = new_pos.pl(Color::Black).board(pc).into_iter().collect();
let intersect = white.intersection(&black).collect::<Vec<_>>();
assert!(
intersect.is_empty(),
"Bitboard in illegal state: {pc:?} collides at {}",
intersect[0]
);
}
}
/// Helper to produce test cases.
fn decondense_moves(
test_case: (&str, Vec<(&str, Vec<&str>, MoveType)>),
@ -1032,6 +1083,21 @@ mod tests {
),
],
),
(
"rnbqkbnr/p1pppppp/8/1P6/8/8/1PPPPPPP/RNBQKBNR b KQkq - 0 2",
vec![
("a7", vec!["a6", "a5"], MoveType::Normal),
("c7", vec!["c6", "c5"], MoveType::Normal),
("d7", vec!["d6", "d5"], MoveType::Normal),
("e7", vec!["e6", "e5"], MoveType::Normal),
("f7", vec!["f6", "f5"], MoveType::Normal),
("g7", vec!["g6", "g5"], MoveType::Normal),
("h7", vec!["h6", "h5"], MoveType::Normal),
("g8", vec!["h6", "f6"], MoveType::Normal),
("b8", vec!["c6", "a6"], MoveType::Normal),
("c8", vec!["b7", "a6"], MoveType::Normal),
],
),
// castling through check
(
"8/8/8/8/8/8/6rr/4K2R w KQ - 0 1",
@ -1230,7 +1296,7 @@ mod tests {
for (move_str, expect_fen) in moves {
let mv = Move::from_uci_algebraic(move_str).unwrap();
eprintln!("Moving {move_str}.");
node = mv.make(node).into();
node = mv.make(&node);
assert_eq!(node.pos.to_fen(), expect_fen.to_string())
}
@ -1245,4 +1311,49 @@ mod tests {
}
}
}
/// The standard movegen test.
///
/// See https://www.chessprogramming.org/Perft
#[test]
fn test_perft() {
// https://www.chessprogramming.org/Perft_Results
let test_cases = [
(
START_POSITION,
// Only up to depth 4 because the engine isn't good enough to do this often
vec![1, 20, 400, 8_902, 197_281],
),
(
"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
vec![1, 48, 2_039, 97862],
),
(
"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
vec![1, 14, 191, 2_812, 43_238],
),
(
"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1",
vec![1, 6, 264, 9467],
),
(
"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8",
vec![1, 44, 1_486, 62_379],
),
(
"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10",
vec![1, 46, 2_079, 89_890],
),
];
for (fen, expected_values) in test_cases {
let root_node = Rc::new(Node {
pos: BoardState::from_fen(fen).unwrap(),
prev: None,
});
for (depth, expected) in expected_values.iter().enumerate() {
assert_eq!(perft(depth, &root_node), *expected, "failed perft depth {depth} on position '{fen}'");
}
}
}
}