chessfriend/position/src/position.rs
Eryn Wells 7744dd06f0 [position, perft] Move Perft into the position crate
Move the Perft trait into the position crate, and let the perft binary call into
that.

Amend Position::make_move to return a bool in the Ok case that indicates whether
the position has been seen before. Use this to decide whether to continue
recursing during the Perft run. I haven't seen that this makes a difference in
the counts returned by Perft yet.
2025-06-16 09:01:58 -07:00

367 lines
11 KiB
Rust

// Eryn Wells <eryn@erynwells.me>
mod captures;
use crate::fen::{FromFenStr, FromFenStrError};
use captures::CapturesList;
use chessfriend_bitboard::BitBoard;
use chessfriend_board::{
display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy,
ZobristState,
};
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_moves::{
algebraic::AlgebraicMoveComponents,
generators::{
AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator,
PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator,
},
GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError,
UnmakeMoveResult, ValidateMove,
};
use std::{collections::HashSet, fmt, sync::Arc};
#[must_use]
#[derive(Clone, Debug, Default, Eq)]
pub struct Position {
pub board: Board,
pub(crate) moves: Vec<MoveRecord>,
pub(crate) captures: CapturesList,
/// A set of hashes of board positions seen throughout the move record.
boards_seen: HashSet<u64>,
}
impl Position {
pub fn empty(zobrist: Option<Arc<ZobristState>>) -> Self {
Self::new(Board::empty(zobrist))
}
/// Return a starting position.
pub fn starting(zobrist: Option<Arc<ZobristState>>) -> Self {
Self::new(Board::starting(zobrist))
}
pub fn new(board: Board) -> Self {
Self {
board,
..Default::default()
}
}
}
impl Position {
/// Place a piece on the board.
///
/// ## Errors
///
/// See [`chessfriend_board::Board::place_piece`].
pub fn place_piece(
&mut self,
piece: Piece,
square: Square,
strategy: PlacePieceStrategy,
) -> Result<Option<Piece>, PlacePieceError> {
self.board.place_piece(piece, square, strategy)
}
#[must_use]
pub fn get_piece(&self, square: Square) -> Option<Piece> {
self.board.get_piece(square)
}
pub fn remove_piece(&mut self, square: Square) -> Option<Piece> {
self.board.remove_piece(square)
}
}
impl Position {
pub fn sight(&self, square: Square) -> BitBoard {
self.board.sight(square)
}
pub fn movement(&self, square: Square) -> BitBoard {
self.board.movement(square)
}
}
impl Position {
pub fn all_moves(&self, color: Option<Color>) -> AllPiecesMoveGenerator {
AllPiecesMoveGenerator::new(&self.board, color)
}
/// Generate legal moves.
///
/// ## Panics
///
/// If the position failed to make a move generated by the internal move
/// generator, this method will panic.
#[must_use]
pub fn all_legal_moves(
&self,
color: Option<Color>,
) -> Box<dyn Iterator<Item = GeneratedMove> + '_> {
let generator = self.all_moves(color);
let mut test_board = self.board.clone();
Box::new(generator.filter(move |ply| {
let active_color_before_move = test_board.active_color();
let ply: Move = ply.clone().into();
let record = test_board
.make_move(ply, ValidateMove::No)
.unwrap_or_else(|err| {
panic!(
"unable to make generated move [{ply}]: {err}\n\n{}",
test_board.display().highlight(ply.relevant_squares())
);
});
let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move));
test_board.unmake_move(&record).unwrap_or_else(|err| {
panic!(
"unable to unmake generated move [{ply}]: {err}\n\n{}",
test_board.display().highlight(ply.relevant_squares())
);
});
move_is_legal
}))
}
#[must_use]
pub fn moves_for_piece(
&self,
square: Square,
) -> Option<Box<dyn Iterator<Item = GeneratedMove>>> {
self.get_piece(square)
.map(|piece| Self::generator(&self.board, piece))
}
#[must_use]
fn generator(board: &Board, piece: Piece) -> Box<dyn Iterator<Item = GeneratedMove>> {
match piece.shape {
Shape::Pawn => Box::new(PawnMoveGenerator::new(board, Some(piece.color))),
Shape::Knight => Box::new(KnightMoveGenerator::new(board, Some(piece.color))),
Shape::Bishop => Box::new(BishopMoveGenerator::new(board, Some(piece.color))),
Shape::Rook => Box::new(RookMoveGenerator::new(board, Some(piece.color))),
Shape::Queen => Box::new(QueenMoveGenerator::new(board, Some(piece.color))),
Shape::King => Box::new(KingMoveGenerator::new(board, Some(piece.color))),
}
}
}
impl Position {
pub fn active_sight(&self) -> BitBoard {
self.board.active_sight()
}
/// A [`BitBoard`] of all squares the given color can see.
pub fn friendly_sight(&self, color: Color) -> BitBoard {
self.board.friendly_sight(color)
}
/// A [`BitBoard`] of all squares visible by colors that oppose the given color.
pub fn active_color_opposing_sight(&self) -> BitBoard {
self.board.active_color_opposing_sight()
}
}
impl Position {
/// Make a move on the board and record it in the move list. Returns `true`
/// if the board position has been seen before (i.e. it's a repetition).
///
/// ## Errors
///
/// Returns one of [`MakeMoveError`] if the move cannot be made.
///
pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<bool, MakeMoveError> {
let record = self.board.make_move(ply, validate)?;
if let Some(captured_piece) = record.captured_piece {
self.captures.push(record.color, captured_piece);
}
let has_seen = if let Some(hash) = self.board.zobrist_hash() {
// HashSet::insert() returns true if the value does not exist in the
// set when it's called.
!self.boards_seen.insert(hash)
} else {
false
};
self.moves.push(record.clone());
Ok(has_seen)
}
/// Unmake the last move made on the board and remove its record from the
/// move list.
///
/// ## Errors
///
/// Returns one of [`UnmakeMoveError`] if the move cannot be made.
///
pub fn unmake_last_move(&mut self) -> UnmakeMoveResult {
let last_move_record = self.moves.pop().ok_or(UnmakeMoveError::NoMove)?;
let hash_before_unmake = self.board.zobrist_hash();
let unmake_result = self.board.unmake_move(&last_move_record);
if unmake_result.is_ok() {
if let Some(capture) = last_move_record.captured_piece {
let popped_piece = self.captures.pop(last_move_record.color);
debug_assert_eq!(Some(capture), popped_piece);
}
if let Some(hash_before_unmake) = hash_before_unmake {
self.boards_seen.remove(&hash_before_unmake);
}
} else {
self.moves.push(last_move_record);
}
unmake_result
}
/// Build a move given its origin, target, and possible promotion. Perform
/// some minimal validation. If a move cannot be
#[must_use]
pub fn move_from_algebraic_components(
&self,
components: AlgebraicMoveComponents,
) -> Option<Move> {
match components {
AlgebraicMoveComponents::Null => Some(Move::null()),
AlgebraicMoveComponents::Regular {
origin,
target,
promotion,
} => self.move_from_origin_target(origin, target, promotion),
}
}
fn move_from_origin_target(
&self,
origin: Square,
target: Square,
promotion: Option<Shape>,
) -> Option<Move> {
let piece = self.get_piece(origin)?;
let color = piece.color;
// Pawn and King are the two most interesting shapes here, because of en
// passant, castling and so on. So, let the move generators do their
// thing and find the move that fits the parameters. For the rest of the
// pieces, do something a little more streamlined.
match piece.shape {
Shape::Pawn => PawnMoveGenerator::new(&self.board, None)
.find(|ply| {
ply.origin() == origin
&& ply.target() == target
&& ply.promotion_shape() == promotion
})
.map(std::convert::Into::into),
Shape::King => KingMoveGenerator::new(&self.board, None)
.find(|ply| ply.origin() == origin && ply.target() == target)
.map(std::convert::Into::into),
_ => {
if color != self.board.active_color() {
return None;
}
let target_bitboard: BitBoard = target.into();
if !(self.movement(origin) & target_bitboard).is_populated() {
return None;
}
if self.get_piece(target).is_some() {
return Some(Move::capture(origin, target));
}
Some(Move::quiet(origin, target))
}
}
}
}
impl Position {
#[must_use]
pub fn zobrist_hash(&self) -> Option<u64> {
self.board.zobrist_hash()
}
pub fn set_zobrist_state(&mut self, state: Arc<ZobristState>) {
self.board.set_zobrist_state(state);
}
}
impl Position {
pub fn display(&self) -> DiagramFormatter {
self.board.display()
}
}
impl FromFenStr for Position {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
let board = Board::from_fen_str(string)?;
Ok(Position::new(board))
}
}
impl ToFenStr for Position {
type Error = <Board as ToFenStr>::Error;
fn to_fen_str(&self) -> Result<String, Self::Error> {
self.board.to_fen_str()
}
}
impl PartialEq for Position {
fn eq(&self, other: &Self) -> bool {
self.board == other.board
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.board.display())?;
if !self.captures.is_empty() {
write!(f, "\n\n{}", self.captures)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_position, Position};
use chessfriend_core::piece;
#[test]
fn piece_on_square() {
let pos = test_position![
Black Bishop on F7,
];
let piece = pos.board.get_piece(Square::F7);
assert_eq!(piece, Some(piece!(Black Bishop)));
}
#[test]
fn piece_in_starting_position() {
let pos = test_position!(starting);
assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook)));
assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook)));
}
}