fix: bitboard not reflecting captures
also implemented perft
This commit is contained in:
parent
8b066b3933
commit
61e5a6114b
19
contrib/bitboard_print.py
Normal file
19
contrib/bitboard_print.py
Normal 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)))
|
24
src/lib.rs
24
src/lib.rs
@ -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 {
|
||||
|
135
src/movegen.rs
135
src/movegen.rs
@ -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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user