stub: proper make/unmake
fails tests
This commit is contained in:
parent
17bec263d9
commit
611cdd4d51
22
src/fen.rs
22
src/fen.rs
@ -1,4 +1,4 @@
|
|||||||
use crate::{BoardState, CastleRights, ColPiece, Color, Square, BOARD_HEIGHT, BOARD_WIDTH};
|
use crate::{Board, CastleRights, ColPiece, Color, Square, BOARD_HEIGHT, BOARD_WIDTH};
|
||||||
|
|
||||||
pub const START_POSITION: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
pub const START_POSITION: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
||||||
|
|
||||||
@ -34,9 +34,9 @@ pub enum FenError {
|
|||||||
InternalError(usize),
|
InternalError(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromFen for BoardState {
|
impl FromFen for Board {
|
||||||
type Error = FenError;
|
type Error = FenError;
|
||||||
fn from_fen(fen: &str) -> Result<BoardState, FenError> {
|
fn from_fen(fen: &str) -> Result<Board, FenError> {
|
||||||
//! Parse FEN string into position.
|
//! Parse FEN string into position.
|
||||||
|
|
||||||
/// Parser state machine.
|
/// Parser state machine.
|
||||||
@ -60,7 +60,7 @@ impl FromFen for BoardState {
|
|||||||
FullMove,
|
FullMove,
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pos = BoardState::default();
|
let mut pos = Board::default();
|
||||||
|
|
||||||
let mut parser_state = FenState::Piece(0, 0);
|
let mut parser_state = FenState::Piece(0, 0);
|
||||||
let mut next_state = FenState::Space;
|
let mut next_state = FenState::Space;
|
||||||
@ -198,7 +198,7 @@ impl FromFen for BoardState {
|
|||||||
}
|
}
|
||||||
FenState::HalfMove => {
|
FenState::HalfMove => {
|
||||||
if let Some(digit) = c.to_digit(10) {
|
if let Some(digit) = c.to_digit(10) {
|
||||||
if pos.half_moves > BoardState::MAX_MOVES {
|
if pos.half_moves > Board::MAX_MOVES {
|
||||||
return Err(FenError::TooManyMoves);
|
return Err(FenError::TooManyMoves);
|
||||||
}
|
}
|
||||||
pos.half_moves *= 10;
|
pos.half_moves *= 10;
|
||||||
@ -211,7 +211,7 @@ impl FromFen for BoardState {
|
|||||||
}
|
}
|
||||||
FenState::FullMove => {
|
FenState::FullMove => {
|
||||||
if let Some(digit) = c.to_digit(10) {
|
if let Some(digit) = c.to_digit(10) {
|
||||||
if pos.half_moves > BoardState::MAX_MOVES {
|
if pos.half_moves > Board::MAX_MOVES {
|
||||||
return Err(FenError::TooManyMoves);
|
return Err(FenError::TooManyMoves);
|
||||||
}
|
}
|
||||||
pos.full_moves *= 10;
|
pos.full_moves *= 10;
|
||||||
@ -233,7 +233,7 @@ impl FromFen for BoardState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToFen for BoardState {
|
impl ToFen for Board {
|
||||||
fn to_fen(&self) -> String {
|
fn to_fen(&self) -> String {
|
||||||
let pieces_str = (0..BOARD_HEIGHT)
|
let pieces_str = (0..BOARD_HEIGHT)
|
||||||
.rev()
|
.rev()
|
||||||
@ -286,7 +286,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_fen_pieces() {
|
fn test_fen_pieces() {
|
||||||
let fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1";
|
let fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1";
|
||||||
let board = BoardState::from_fen(fen.into()).unwrap();
|
let board = Board::from_fen(fen.into()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(0..N_SQUARES)
|
(0..N_SQUARES)
|
||||||
.map(Square)
|
.map(Square)
|
||||||
@ -388,7 +388,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fen_half_move_counter() {
|
fn test_fen_half_move_counter() {
|
||||||
for i in 0..=BoardState::MAX_MOVES {
|
for i in 0..=Board::MAX_MOVES {
|
||||||
let board = make_board!("8/8/8/8/8/8/8/8 w - - {i} 0");
|
let board = make_board!("8/8/8/8/8/8/8/8 w - - {i} 0");
|
||||||
assert_eq!(board.half_moves, i);
|
assert_eq!(board.half_moves, i);
|
||||||
assert_eq!(board.full_moves, 0);
|
assert_eq!(board.full_moves, 0);
|
||||||
@ -397,7 +397,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fen_move_counter() {
|
fn test_fen_move_counter() {
|
||||||
for i in 0..=BoardState::MAX_MOVES {
|
for i in 0..=Board::MAX_MOVES {
|
||||||
let board = make_board!("8/8/8/8/8/8/8/8 w - - 0 {i}");
|
let board = make_board!("8/8/8/8/8/8/8/8 w - - 0 {i}");
|
||||||
assert_eq!(board.half_moves, 0);
|
assert_eq!(board.half_moves, 0);
|
||||||
assert_eq!(board.full_moves, i);
|
assert_eq!(board.full_moves, i);
|
||||||
@ -437,7 +437,7 @@ mod tests {
|
|||||||
|
|
||||||
for fen1 in test_cases {
|
for fen1 in test_cases {
|
||||||
println!("fen1: {fen1:?}");
|
println!("fen1: {fen1:?}");
|
||||||
let fen2 = BoardState::from_fen(fen1).unwrap().to_fen();
|
let fen2 = Board::from_fen(fen1).unwrap().to_fen();
|
||||||
|
|
||||||
assert_eq!(fen1.to_string(), fen2, "FEN not equivalent")
|
assert_eq!(fen1.to_string(), fen2, "FEN not equivalent")
|
||||||
}
|
}
|
||||||
|
50
src/lib.rs
50
src/lib.rs
@ -6,6 +6,8 @@ use std::str::FromStr;
|
|||||||
pub mod fen;
|
pub mod fen;
|
||||||
pub mod movegen;
|
pub mod movegen;
|
||||||
|
|
||||||
|
use crate::fen::{FromFen, START_POSITION};
|
||||||
|
|
||||||
const BOARD_WIDTH: usize = 8;
|
const BOARD_WIDTH: usize = 8;
|
||||||
const BOARD_HEIGHT: usize = 8;
|
const BOARD_HEIGHT: usize = 8;
|
||||||
const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT;
|
const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT;
|
||||||
@ -401,7 +403,7 @@ impl ToString for CastleRights {
|
|||||||
///
|
///
|
||||||
/// Default is empty.
|
/// Default is empty.
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct BoardState {
|
pub struct Board {
|
||||||
/// Player bitboards
|
/// Player bitboards
|
||||||
players: [Player; N_COLORS],
|
players: [Player; N_COLORS],
|
||||||
|
|
||||||
@ -426,11 +428,12 @@ pub struct BoardState {
|
|||||||
turn: Color,
|
turn: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Piece missing where there should be one.
|
impl Board {
|
||||||
#[derive(Debug)]
|
/// Default chess position.
|
||||||
struct NoPieceError;
|
pub fn starting_pos() -> Self {
|
||||||
|
Board::from_fen(START_POSITION).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
impl BoardState {
|
|
||||||
/// Get mutable reference to a player.
|
/// Get mutable reference to a player.
|
||||||
fn pl_mut(&mut self, col: Color) -> &mut Player {
|
fn pl_mut(&mut self, col: Color) -> &mut Player {
|
||||||
&mut self.players[col as usize]
|
&mut self.players[col as usize]
|
||||||
@ -456,42 +459,41 @@ impl BoardState {
|
|||||||
(0..N_SQUARES).map(Square::try_from).map(|x| x.unwrap())
|
(0..N_SQUARES).map(Square::try_from).map(|x| x.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new piece in a location.
|
/// Create a new piece in a location, and pop any existing piece in the destination.
|
||||||
fn set_piece(&mut self, idx: Square, pc: ColPiece) {
|
fn set_piece(&mut self, idx: Square, pc: ColPiece) -> Option<ColPiece> {
|
||||||
let _ = self.del_piece(idx);
|
let dest_pc = self.del_piece(idx);
|
||||||
let pl = self.pl_mut(pc.col);
|
let pl = self.pl_mut(pc.col);
|
||||||
pl.board_mut(pc.into()).on_sq(idx);
|
pl.board_mut(pc.into()).on_sq(idx);
|
||||||
*self.mail.sq_mut(idx) = Some(pc);
|
*self.mail.sq_mut(idx) = Some(pc);
|
||||||
|
dest_pc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the piece (or no piece) in a square.
|
/// Set the piece (or no piece) in a square, and return ("pop") the existing piece.
|
||||||
fn set_square(&mut self, idx: Square, pc: Option<ColPiece>) {
|
fn set_square(&mut self, idx: Square, pc: Option<ColPiece>) -> Option<ColPiece> {
|
||||||
match pc {
|
match pc {
|
||||||
Some(pc) => self.set_piece(idx, pc),
|
Some(pc) => {self.set_piece(idx, pc)},
|
||||||
None => {
|
None => {
|
||||||
let _ = self.del_piece(idx);
|
self.del_piece(idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the piece in a location, and return ("pop") that piece.
|
/// Delete the piece in a location, and return ("pop") that piece.
|
||||||
///
|
fn del_piece(&mut self, idx: Square) -> Option<ColPiece> {
|
||||||
/// Returns an error if there is no piece in the location.
|
|
||||||
fn del_piece(&mut self, idx: Square) -> Result<ColPiece, NoPieceError> {
|
|
||||||
if let Some(pc) = *self.mail.sq_mut(idx) {
|
if let Some(pc) = *self.mail.sq_mut(idx) {
|
||||||
let pl = self.pl_mut(pc.col);
|
let pl = self.pl_mut(pc.col);
|
||||||
pl.board_mut(pc.into()).off_sq(idx);
|
pl.board_mut(pc.into()).off_sq(idx);
|
||||||
*self.mail.sq_mut(idx) = None;
|
*self.mail.sq_mut(idx) = None;
|
||||||
Ok(pc)
|
Some(pc)
|
||||||
} else {
|
} else {
|
||||||
Err(NoPieceError)
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_piece(&mut self, src: Square, dest: Square) {
|
fn move_piece(&mut self, src: Square, dest: Square) {
|
||||||
let pc = self
|
let pc = self
|
||||||
.del_piece(src)
|
.del_piece(src)
|
||||||
.unwrap_or_else(|_| panic!("move ({src} -> {dest}) should have piece at source"));
|
.unwrap_or_else(|| panic!("move ({src} -> {dest}) should have piece at source"));
|
||||||
self.set_piece(dest, pc);
|
self.set_piece(dest, pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -516,12 +518,12 @@ impl BoardState {
|
|||||||
|
|
||||||
new_board.castle.0.reverse();
|
new_board.castle.0.reverse();
|
||||||
|
|
||||||
for sq in BoardState::squares() {
|
for sq in Board::squares() {
|
||||||
let opt_pc = self.get_piece(sq.mirror_vert()).map(|pc| ColPiece {
|
let opt_pc = self.get_piece(sq.mirror_vert()).map(|pc| ColPiece {
|
||||||
col: pc.col.flip(),
|
col: pc.col.flip(),
|
||||||
pc: pc.pc,
|
pc: pc.pc,
|
||||||
});
|
});
|
||||||
new_board.set_square(sq, opt_pc)
|
new_board.set_square(sq, opt_pc);
|
||||||
}
|
}
|
||||||
new_board
|
new_board
|
||||||
}
|
}
|
||||||
@ -530,7 +532,7 @@ impl BoardState {
|
|||||||
const MAX_MOVES: usize = 9_999;
|
const MAX_MOVES: usize = 9_999;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl core::fmt::Display for BoardState {
|
impl core::fmt::Display for Board {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let mut str = String::with_capacity(N_SQUARES + BOARD_HEIGHT);
|
let mut str = String::with_capacity(N_SQUARES + BOARD_HEIGHT);
|
||||||
for row in (0..BOARD_HEIGHT).rev() {
|
for row in (0..BOARD_HEIGHT).rev() {
|
||||||
@ -619,7 +621,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let board = BoardState::from_fen("8/4p3/1q1Q1p2/4p3/1p1r4/8/8/8 w - - 0 1").unwrap();
|
let board = Board::from_fen("8/4p3/1q1Q1p2/4p3/1p1r4/8/8/8 w - - 0 1").unwrap();
|
||||||
let white_queens = board
|
let white_queens = board
|
||||||
.pl(Color::White)
|
.pl(Color::White)
|
||||||
.board(Piece::Queen)
|
.board(Piece::Queen)
|
||||||
@ -654,8 +656,8 @@ mod tests {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (tc, expect) in test_cases {
|
for (tc, expect) in test_cases {
|
||||||
let tc = BoardState::from_fen(tc).unwrap();
|
let tc = Board::from_fen(tc).unwrap();
|
||||||
let expect = BoardState::from_fen(expect).unwrap();
|
let expect = Board::from_fen(expect).unwrap();
|
||||||
assert_eq!(tc.flip_colors(), expect);
|
assert_eq!(tc.flip_colors(), expect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
315
src/movegen.rs
315
src/movegen.rs
@ -2,46 +2,11 @@
|
|||||||
|
|
||||||
use crate::fen::{FromFen, ToFen, START_POSITION};
|
use crate::fen::{FromFen, ToFen, START_POSITION};
|
||||||
use crate::{
|
use crate::{
|
||||||
BoardState, ColPiece, Color, Piece, Square, SquareError, BOARD_HEIGHT, BOARD_WIDTH, N_SQUARES,
|
Board, CastleRights, ColPiece, Color, Piece, Square, SquareError, BOARD_HEIGHT,
|
||||||
|
BOARD_WIDTH, N_SQUARES,
|
||||||
};
|
};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// Game tree node.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Node {
|
|
||||||
/// Immutable position data.
|
|
||||||
pos: BoardState,
|
|
||||||
/// Backlink to previous node.
|
|
||||||
prev: Option<Rc<Node>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Node {
|
|
||||||
fn default() -> Self {
|
|
||||||
Node::new(BoardState::from_fen(START_POSITION).expect("Starting FEN should be valid"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
/// Undo move.
|
|
||||||
///
|
|
||||||
/// 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> {
|
|
||||||
if let Some(prev) = &self.prev {
|
|
||||||
Rc::clone(prev)
|
|
||||||
} else {
|
|
||||||
panic!("unmake should not be called on root node");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(board: BoardState) -> Self {
|
|
||||||
Node {
|
|
||||||
pos: board,
|
|
||||||
prev: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Piece enum specifically for promotions.
|
/// Piece enum specifically for promotions.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
enum PromotePiece {
|
enum PromotePiece {
|
||||||
@ -62,6 +27,91 @@ impl From<PromotePiece> for Piece {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AntiMoveType {
|
||||||
|
Normal,
|
||||||
|
/// En passant.
|
||||||
|
EnPassant {
|
||||||
|
cap: Square,
|
||||||
|
},
|
||||||
|
/// Pawn promotion.
|
||||||
|
Promotion,
|
||||||
|
/// King-rook castle. The king is the one considered to move.
|
||||||
|
Castle {
|
||||||
|
rook_src: Square,
|
||||||
|
rook_dest: Square,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information used to reverse (unmake) a move.
|
||||||
|
pub struct AntiMove {
|
||||||
|
dest: Square,
|
||||||
|
src: Square,
|
||||||
|
/// Captured piece, always assumed to be of enemy color.
|
||||||
|
cap: Option<Piece>,
|
||||||
|
move_type: AntiMoveType,
|
||||||
|
/// Half-move counter prior to this move
|
||||||
|
half_moves: usize,
|
||||||
|
/// Castling rights prior to this move.
|
||||||
|
castle: CastleRights,
|
||||||
|
/// En passant target square prior to this move.
|
||||||
|
ep_square: Option<Square>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AntiMove {
|
||||||
|
/// Undo the move.
|
||||||
|
fn unmake(self, pos: &mut Board) {
|
||||||
|
pos.move_piece(self.dest, self.src);
|
||||||
|
pos.half_moves = self.half_moves;
|
||||||
|
pos.castle = self.castle;
|
||||||
|
pos.ep_square = self.ep_square;
|
||||||
|
|
||||||
|
/// Restore captured piece at a given square.
|
||||||
|
macro_rules! cap_sq {
|
||||||
|
($sq: expr) => {
|
||||||
|
if let Some(cap_pc) = self.cap {
|
||||||
|
pos.set_piece(
|
||||||
|
$sq,
|
||||||
|
ColPiece {
|
||||||
|
pc: cap_pc,
|
||||||
|
col: pos.turn.flip(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.turn = pos.turn.flip();
|
||||||
|
if pos.turn == Color::Black {
|
||||||
|
pos.full_moves -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.move_type {
|
||||||
|
AntiMoveType::Normal => {
|
||||||
|
cap_sq!(self.dest)
|
||||||
|
}
|
||||||
|
AntiMoveType::EnPassant { cap } => {
|
||||||
|
cap_sq!(cap);
|
||||||
|
}
|
||||||
|
AntiMoveType::Promotion => {
|
||||||
|
cap_sq!(self.dest);
|
||||||
|
pos.set_piece(
|
||||||
|
self.src,
|
||||||
|
ColPiece {
|
||||||
|
pc: Piece::Pawn,
|
||||||
|
col: pos.turn,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AntiMoveType::Castle {
|
||||||
|
rook_src,
|
||||||
|
rook_dest,
|
||||||
|
} => {
|
||||||
|
pos.move_piece(rook_dest, rook_src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
|
||||||
enum MoveType {
|
enum MoveType {
|
||||||
/// Pawn promotes to another piece.
|
/// Pawn promotes to another piece.
|
||||||
@ -80,24 +130,30 @@ pub struct Move {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Move {
|
impl Move {
|
||||||
/// Make move, without setting up the backlink for unmake.
|
/// Apply move to a position.
|
||||||
///
|
fn make(self, pos: &mut Board) -> AntiMove {
|
||||||
/// Call this directly when making new positions that are dead ends (won't be used further).
|
let mut anti_move = AntiMove {
|
||||||
fn make_unlinked(self, old_pos: &BoardState) -> BoardState {
|
dest: self.dest,
|
||||||
let mut new_pos = *old_pos;
|
src: self.src,
|
||||||
|
cap: None,
|
||||||
|
move_type: AntiMoveType::Normal,
|
||||||
|
half_moves: pos.half_moves,
|
||||||
|
castle: pos.castle,
|
||||||
|
ep_square: pos.ep_square,
|
||||||
|
};
|
||||||
|
|
||||||
// reset en passant
|
// reset en passant
|
||||||
new_pos.ep_square = None;
|
let ep_square = pos.ep_square;
|
||||||
|
pos.ep_square = None;
|
||||||
|
|
||||||
if old_pos.turn == Color::Black {
|
if pos.turn == Color::Black {
|
||||||
new_pos.full_moves += 1;
|
pos.full_moves += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the piece at the source square.
|
/// Get the piece at the source square.
|
||||||
macro_rules! pc_src {
|
macro_rules! pc_src {
|
||||||
($data: ident) => {
|
($data: ident) => {
|
||||||
new_pos
|
pos.get_piece($data.src)
|
||||||
.get_piece($data.src)
|
|
||||||
.expect("Move source should have a piece")
|
.expect("Move source should have a piece")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -106,11 +162,11 @@ impl Move {
|
|||||||
($pc_src: ident) => {
|
($pc_src: ident) => {
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
$pc_src.col,
|
$pc_src.col,
|
||||||
new_pos.turn,
|
pos.turn,
|
||||||
"Moving piece on wrong turn. Move {} -> {} on board '{}'",
|
"Moving piece on wrong turn. Move {} -> {} on board '{}'",
|
||||||
self.src,
|
self.src,
|
||||||
self.dest,
|
self.dest,
|
||||||
old_pos.to_fen()
|
pos.to_fen()
|
||||||
);
|
);
|
||||||
debug_assert_ne!(self.src, self.dest, "Moving piece to itself.");
|
debug_assert_ne!(self.src, self.dest, "Moving piece to itself.");
|
||||||
};
|
};
|
||||||
@ -122,27 +178,32 @@ impl Move {
|
|||||||
pc_asserts!(pc_src);
|
pc_asserts!(pc_src);
|
||||||
debug_assert_eq!(pc_src.pc, Piece::Pawn);
|
debug_assert_eq!(pc_src.pc, Piece::Pawn);
|
||||||
|
|
||||||
let _ = new_pos.del_piece(self.src);
|
pos.half_moves = 0;
|
||||||
new_pos.set_piece(
|
|
||||||
|
anti_move.move_type = AntiMoveType::Promotion;
|
||||||
|
|
||||||
|
pos.del_piece(self.src);
|
||||||
|
pos.set_piece(
|
||||||
self.dest,
|
self.dest,
|
||||||
ColPiece {
|
ColPiece {
|
||||||
pc: Piece::from(to_piece),
|
pc: Piece::from(to_piece),
|
||||||
col: pc_src.col,
|
col: pc_src.col,
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
MoveType::Normal => {
|
MoveType::Normal => {
|
||||||
let pc_src = pc_src!(self);
|
let pc_src = pc_src!(self);
|
||||||
pc_asserts!(pc_src);
|
pc_asserts!(pc_src);
|
||||||
|
|
||||||
let pc_dest: Option<ColPiece> = new_pos.get_piece(self.dest);
|
let pc_dest: Option<ColPiece> = pos.get_piece(self.dest);
|
||||||
|
anti_move.cap = pc_dest.map(|pc| pc.pc);
|
||||||
|
|
||||||
let (src_row, src_col) = self.src.to_row_col_signed();
|
let (src_row, src_col) = self.src.to_row_col_signed();
|
||||||
let (dest_row, dest_col) = self.dest.to_row_col_signed();
|
let (dest_row, dest_col) = self.dest.to_row_col_signed();
|
||||||
|
|
||||||
if matches!(pc_src.pc, Piece::Pawn) {
|
if matches!(pc_src.pc, Piece::Pawn) {
|
||||||
// pawn moves are irreversible
|
// pawn moves are irreversible
|
||||||
new_pos.half_moves = 0;
|
pos.half_moves = 0;
|
||||||
|
|
||||||
// set en-passant target square
|
// set en-passant target square
|
||||||
if src_row.abs_diff(dest_row) == 2 {
|
if src_row.abs_diff(dest_row) == 2 {
|
||||||
@ -155,31 +216,46 @@ impl Move {
|
|||||||
};
|
};
|
||||||
let ep_targ = Square::from_row_col_signed(ep_row, ep_col)
|
let ep_targ = Square::from_row_col_signed(ep_row, ep_col)
|
||||||
.expect("En-passant target should be valid.");
|
.expect("En-passant target should be valid.");
|
||||||
new_pos.ep_square = Some(ep_targ)
|
pos.ep_square = Some(ep_targ)
|
||||||
} else if pc_dest.is_none() && src_col != dest_col {
|
} else if pc_dest.is_none() && src_col != dest_col {
|
||||||
// we took en passant
|
// we took en passant
|
||||||
debug_assert!(src_row.abs_diff(dest_row) == 1);
|
debug_assert!(src_row.abs_diff(dest_row) == 1);
|
||||||
debug_assert_eq!(self.dest, old_pos.ep_square.unwrap());
|
assert_eq!(self.dest, ep_square.expect("ep target should exist if taking ep"));
|
||||||
// square to actually capture at
|
// square to actually capture at
|
||||||
let ep_capture = Square::try_from(match pc_src.col {
|
let ep_capture = Square::try_from(match pc_src.col {
|
||||||
Color::White => self.dest.0 - BOARD_WIDTH,
|
Color::White => self.dest.0 - BOARD_WIDTH,
|
||||||
Color::Black => self.dest.0 + BOARD_WIDTH,
|
Color::Black => self.dest.0 + BOARD_WIDTH,
|
||||||
})
|
})
|
||||||
.expect("En-passant capture square should be valid");
|
.expect("En-passant capture square should be valid");
|
||||||
new_pos.del_piece(ep_capture).unwrap_or_else(|_| {
|
|
||||||
panic!("En-passant capture square should have piece. Position '{}', move {:?}", old_pos.to_fen(), self)
|
anti_move.move_type = AntiMoveType::EnPassant{cap: ep_capture};
|
||||||
});
|
if let Some(pc_cap) = pos.del_piece(ep_capture) {
|
||||||
|
debug_assert_eq!(
|
||||||
|
pc_cap.col,
|
||||||
|
pos.turn.flip(),
|
||||||
|
"attempt to en passant wrong color, pos '{}', move {:?}",
|
||||||
|
pos.to_fen(),
|
||||||
|
self
|
||||||
|
);
|
||||||
|
anti_move.cap = Some(pc_cap.pc);
|
||||||
|
} else {
|
||||||
|
panic!(
|
||||||
|
"En-passant capture square should have piece. Position '{}', move {:?}",
|
||||||
|
pos.to_fen(),
|
||||||
|
self
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
new_pos.half_moves += 1;
|
pos.half_moves += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if pc_dest.is_some() {
|
if pc_dest.is_some() {
|
||||||
// captures are irreversible
|
// captures are irreversible
|
||||||
new_pos.half_moves = 0;
|
pos.half_moves = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let castle = &mut new_pos.pl_castle_mut(pc_src.col);
|
let castle = &mut pos.pl_castle_mut(pc_src.col);
|
||||||
if matches!(pc_src.pc, Piece::King) {
|
if matches!(pc_src.pc, Piece::King) {
|
||||||
// forfeit castling rights
|
// forfeit castling rights
|
||||||
castle.k = false;
|
castle.k = false;
|
||||||
@ -203,8 +279,12 @@ impl Move {
|
|||||||
.expect("rook castling src square should be valid");
|
.expect("rook castling src square should be valid");
|
||||||
let rook_dest = Square::from_row_col_signed(rook_row, rook_dest_col)
|
let rook_dest = Square::from_row_col_signed(rook_row, rook_dest_col)
|
||||||
.expect("rook castling dest square should be valid");
|
.expect("rook castling dest square should be valid");
|
||||||
debug_assert!(new_pos.get_piece(rook_src).is_some(), "rook castling src square has no rook (move: {rook_src} -> {rook_dest})");
|
debug_assert!(pos.get_piece(rook_src).is_some(), "rook castling src square has no rook (move: {rook_src} -> {rook_dest})");
|
||||||
new_pos.move_piece(rook_src, rook_dest);
|
anti_move.move_type = AntiMoveType::Castle {
|
||||||
|
rook_src,
|
||||||
|
rook_dest,
|
||||||
|
};
|
||||||
|
pos.move_piece(rook_src, rook_dest);
|
||||||
}
|
}
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
(0..=2).contains(&horiz_diff),
|
(0..=2).contains(&horiz_diff),
|
||||||
@ -231,26 +311,13 @@ impl Move {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new_pos.move_piece(self.src, self.dest);
|
pos.move_piece(self.src, self.dest);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
new_pos.turn = new_pos.turn.flip();
|
pos.turn = pos.turn.flip();
|
||||||
|
|
||||||
new_pos
|
anti_move
|
||||||
}
|
|
||||||
|
|
||||||
/// Make move and return new position.
|
|
||||||
///
|
|
||||||
/// 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>) -> Rc<Node> {
|
|
||||||
let pos = self.make_unlinked(&old_node.pos);
|
|
||||||
Node {
|
|
||||||
prev: Some(Rc::clone(old_node)),
|
|
||||||
pos,
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +437,7 @@ enum SliderDirection {
|
|||||||
/// * `slide_type`: Directions the piece is allowed to go in.
|
/// * `slide_type`: Directions the piece is allowed to go in.
|
||||||
/// * `keep_going`: Allow sliding more than one square (true for everything except king).
|
/// * `keep_going`: Allow sliding more than one square (true for everything except king).
|
||||||
fn move_slider(
|
fn move_slider(
|
||||||
board: &BoardState,
|
board: &Board,
|
||||||
src: Square,
|
src: Square,
|
||||||
move_list: &mut Vec<Move>,
|
move_list: &mut Vec<Move>,
|
||||||
slide_type: SliderDirection,
|
slide_type: SliderDirection,
|
||||||
@ -414,7 +481,7 @@ fn move_slider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PseudoMoveGen for BoardState {
|
impl PseudoMoveGen for Board {
|
||||||
fn gen_pseudo_moves(&self) -> impl IntoIterator<Item = Move> {
|
fn gen_pseudo_moves(&self) -> impl IntoIterator<Item = Move> {
|
||||||
let mut ret = Vec::new();
|
let mut ret = Vec::new();
|
||||||
let pl = self.pl(self.turn);
|
let pl = self.pl(self.turn);
|
||||||
@ -607,7 +674,7 @@ pub trait LegalMoveGen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Is a given player in check?
|
/// Is a given player in check?
|
||||||
fn is_check(board: &BoardState, pl: Color) -> bool {
|
fn is_check(board: &Board, pl: Color) -> bool {
|
||||||
for src in board.pl(pl).board(Piece::King).into_iter() {
|
for src in board.pl(pl).board(Piece::King).into_iter() {
|
||||||
macro_rules! detect_checker {
|
macro_rules! detect_checker {
|
||||||
($dirs: ident, $pc: pat, $keep_going: expr) => {
|
($dirs: ident, $pc: pat, $keep_going: expr) => {
|
||||||
@ -653,42 +720,47 @@ fn is_check(board: &BoardState, pl: Color) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LegalMoveGen for Node {
|
impl LegalMoveGen for Board {
|
||||||
|
// mut required for check checking
|
||||||
fn gen_moves(&self) -> impl IntoIterator<Item = Move> {
|
fn gen_moves(&self) -> impl IntoIterator<Item = Move> {
|
||||||
self.pos
|
let mut pos = self.clone();
|
||||||
.gen_pseudo_moves()
|
|
||||||
|
pos.gen_pseudo_moves()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|mv| {
|
.filter(|mv| {
|
||||||
// disallow friendly fire
|
// disallow friendly fire
|
||||||
let src_pc = self
|
let src_pc = self
|
||||||
.pos
|
|
||||||
.get_piece(mv.src)
|
.get_piece(mv.src)
|
||||||
.expect("move source should have piece");
|
.expect("move source should have piece");
|
||||||
if let Some(dest_pc) = self.pos.get_piece(mv.dest) {
|
if let Some(dest_pc) = self.get_piece(mv.dest) {
|
||||||
return dest_pc.col != src_pc.col;
|
return dest_pc.col != src_pc.col;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.filter(|mv| {
|
.filter(move |mv| {
|
||||||
// disallow moving into check
|
// disallow moving into check
|
||||||
let new_pos = mv.make_unlinked(&self.pos);
|
let anti_move = mv.make(&mut pos);
|
||||||
!is_check(&new_pos, self.pos.turn)
|
let ret = !is_check(&pos, self.turn);
|
||||||
|
anti_move.unmake(&mut pos);
|
||||||
|
ret
|
||||||
})
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many nodes at depth N can be reached from this position.
|
/// How many nodes at depth N can be reached from this position.
|
||||||
pub fn perft(depth: usize, node: &Rc<Node>) -> usize {
|
pub fn perft(depth: usize, pos: &mut Board) -> usize {
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut ans = 0;
|
let mut ans = 0;
|
||||||
|
|
||||||
let moves = node.gen_moves();
|
let moves: Vec<Move> = pos.gen_moves().into_iter().collect();
|
||||||
for mv in moves {
|
for mv in moves {
|
||||||
let new_node = mv.make(node);
|
let anti_move = mv.make(pos);
|
||||||
ans += perft(depth - 1, &new_node);
|
ans += perft(depth - 1, pos);
|
||||||
|
anti_move.unmake(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
ans
|
ans
|
||||||
@ -702,15 +774,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
/// Ensure that bitboard properly reflects captures.
|
/// Ensure that bitboard properly reflects captures.
|
||||||
fn test_bitboard_capture() {
|
fn test_bitboard_capture() {
|
||||||
let board = BoardState::from_fen("8/8/8/8/8/8/r7/R7 w - - 0 1").unwrap();
|
let mut pos = Board::from_fen("8/8/8/8/8/8/r7/R7 w - - 0 1").unwrap();
|
||||||
let mv = Move::from_uci_algebraic("a1a2").unwrap();
|
let mv = Move::from_uci_algebraic("a1a2").unwrap();
|
||||||
let new_pos = mv.make_unlinked(&board);
|
let anti_move = mv.make(&mut pos);
|
||||||
|
|
||||||
use std::collections::hash_set::HashSet;
|
use std::collections::hash_set::HashSet;
|
||||||
use Piece::*;
|
use Piece::*;
|
||||||
for pc in [Rook, Bishop, Knight, Queen, King, Pawn] {
|
for pc in [Rook, Bishop, Knight, Queen, King, Pawn] {
|
||||||
let white: HashSet<_> = new_pos.pl(Color::White).board(pc).into_iter().collect();
|
let white: HashSet<_> = pos.pl(Color::White).board(pc).into_iter().collect();
|
||||||
let black: HashSet<_> = new_pos.pl(Color::Black).board(pc).into_iter().collect();
|
let black: HashSet<_> = pos.pl(Color::Black).board(pc).into_iter().collect();
|
||||||
let intersect = white.intersection(&black).collect::<Vec<_>>();
|
let intersect = white.intersection(&black).collect::<Vec<_>>();
|
||||||
assert!(
|
assert!(
|
||||||
intersect.is_empty(),
|
intersect.is_empty(),
|
||||||
@ -723,9 +795,9 @@ mod tests {
|
|||||||
/// Helper to produce test cases.
|
/// Helper to produce test cases.
|
||||||
fn decondense_moves(
|
fn decondense_moves(
|
||||||
test_case: (&str, Vec<(&str, Vec<&str>, MoveType)>),
|
test_case: (&str, Vec<(&str, Vec<&str>, MoveType)>),
|
||||||
) -> (BoardState, Vec<Move>) {
|
) -> (Board, Vec<Move>) {
|
||||||
let (fen, expected) = test_case;
|
let (fen, expected) = test_case;
|
||||||
let board = BoardState::from_fen(fen).unwrap();
|
let board = Board::from_fen(fen).unwrap();
|
||||||
|
|
||||||
let mut expected_moves = expected
|
let mut expected_moves = expected
|
||||||
.iter()
|
.iter()
|
||||||
@ -749,7 +821,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate new test cases by flipping colors on existing ones.
|
/// Generate new test cases by flipping colors on existing ones.
|
||||||
fn flip_test_case(board: BoardState, moves: &Vec<Move>) -> (BoardState, Vec<Move>) {
|
fn flip_test_case(board: Board, moves: &Vec<Move>) -> (Board, Vec<Move>) {
|
||||||
let mut move_vec = moves
|
let mut move_vec = moves
|
||||||
.iter()
|
.iter()
|
||||||
.map(|mv| Move {
|
.map(|mv| Move {
|
||||||
@ -1044,7 +1116,7 @@ mod tests {
|
|||||||
|
|
||||||
let all_cases = check_cases.iter().chain(¬_check_cases);
|
let all_cases = check_cases.iter().chain(¬_check_cases);
|
||||||
for (fen, expected) in all_cases {
|
for (fen, expected) in all_cases {
|
||||||
let board = BoardState::from_fen(fen).unwrap();
|
let board = Board::from_fen(fen).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
is_check(&board, Color::White),
|
is_check(&board, Color::White),
|
||||||
*expected,
|
*expected,
|
||||||
@ -1163,14 +1235,12 @@ mod tests {
|
|||||||
|
|
||||||
let all_cases = [augmented_test_cases, test_cases].concat();
|
let all_cases = [augmented_test_cases, test_cases].concat();
|
||||||
|
|
||||||
for (board, mut expected_moves) in all_cases {
|
for (mut board, mut expected_moves) in all_cases {
|
||||||
eprintln!("on test '{}'", board.to_fen());
|
eprintln!("on test '{}'", board.to_fen());
|
||||||
expected_moves.sort_unstable();
|
expected_moves.sort_unstable();
|
||||||
let expected_moves = expected_moves;
|
let expected_moves = expected_moves;
|
||||||
|
|
||||||
let node = Node::new(board);
|
let mut moves: Vec<Move> = board.gen_moves().into_iter().collect();
|
||||||
|
|
||||||
let mut moves: Vec<Move> = node.gen_moves().into_iter().collect();
|
|
||||||
moves.sort_unstable();
|
moves.sort_unstable();
|
||||||
let moves = moves;
|
let moves = moves;
|
||||||
|
|
||||||
@ -1290,24 +1360,19 @@ mod tests {
|
|||||||
for (i, test_case) in test_cases.iter().enumerate() {
|
for (i, test_case) in test_cases.iter().enumerate() {
|
||||||
let (start_pos, moves) = test_case;
|
let (start_pos, moves) = test_case;
|
||||||
|
|
||||||
// make move
|
|
||||||
eprintln!("Starting test case {i}, make move.");
|
eprintln!("Starting test case {i}, make move.");
|
||||||
let mut node = Rc::new(Node::new(BoardState::from_fen(start_pos).unwrap()));
|
let mut pos = Board::from_fen(start_pos).unwrap();
|
||||||
for (move_str, expect_fen) in moves {
|
for (move_str, expect_fen) in moves {
|
||||||
|
let prior_fen = pos.to_fen();
|
||||||
let mv = Move::from_uci_algebraic(move_str).unwrap();
|
let mv = Move::from_uci_algebraic(move_str).unwrap();
|
||||||
eprintln!("Moving {move_str}.");
|
eprintln!("Moving {move_str} on {}", prior_fen);
|
||||||
node = mv.make(&node);
|
let anti_move = mv.make(&mut pos);
|
||||||
assert_eq!(node.pos.to_fen(), expect_fen.to_string())
|
eprintln!("Unmaking {move_str} on {}.", pos.to_fen());
|
||||||
}
|
anti_move.unmake(&mut pos);
|
||||||
|
assert_eq!(pos.to_fen(), prior_fen.to_string());
|
||||||
// unmake move
|
eprintln!("Remaking {move_str}.");
|
||||||
eprintln!("Starting test case {i}, unmake move.");
|
let anti_move = mv.make(&mut pos);
|
||||||
for (_, expect_fen) in moves.iter().rev().chain([("", *start_pos)].iter()) {
|
assert_eq!(pos.to_fen(), expect_fen.to_string());
|
||||||
eprintln!("{}", expect_fen);
|
|
||||||
assert_eq!(*node.pos.to_fen(), expect_fen.to_string());
|
|
||||||
if *expect_fen != *start_pos {
|
|
||||||
node = node.unmake();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1354,7 +1419,7 @@ mod tests {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
for (fen, expected_values, _debug_limit_depth) in test_cases {
|
for (fen, expected_values, _debug_limit_depth) in test_cases {
|
||||||
let root_node = Rc::new(Node::new(BoardState::from_fen(fen).unwrap()));
|
let mut pos = Board::from_fen(fen).unwrap();
|
||||||
|
|
||||||
for (depth, expected) in expected_values.iter().enumerate() {
|
for (depth, expected) in expected_values.iter().enumerate() {
|
||||||
eprintln!("running perft depth {depth} on position '{fen}'");
|
eprintln!("running perft depth {depth} on position '{fen}'");
|
||||||
@ -1364,7 +1429,7 @@ mod tests {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(perft(depth, &root_node), *expected,);
|
assert_eq!(perft(depth, &mut pos), *expected,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user