2024-01-08 14:41:54 -08:00
|
|
|
// Eryn Wells <eryn@erynwells.me>
|
|
|
|
|
2025-05-27 11:49:33 -07:00
|
|
|
mod captures;
|
2023-12-26 11:25:27 -07:00
|
|
|
|
2025-06-07 08:55:34 -07:00
|
|
|
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};
|
2025-05-28 16:25:55 -07:00
|
|
|
use chessfriend_moves::{
|
|
|
|
generators::{
|
|
|
|
AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator,
|
|
|
|
PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator,
|
|
|
|
},
|
2025-06-01 19:02:53 -07:00
|
|
|
GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError,
|
|
|
|
UnmakeMoveResult, ValidateMove,
|
2025-05-28 16:25:55 -07:00
|
|
|
};
|
2025-06-07 08:55:34 -07:00
|
|
|
use std::{collections::HashSet, fmt, sync::Arc};
|
2025-05-27 11:59:42 -07:00
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
#[derive(Clone, Debug, Default, Eq)]
|
|
|
|
pub struct Position {
|
|
|
|
pub board: Board,
|
|
|
|
pub(crate) moves: Vec<MoveRecord>,
|
|
|
|
pub(crate) captures: CapturesList,
|
2025-06-07 08:55:34 -07:00
|
|
|
|
|
|
|
/// A set of hashes of board positions seen throughout the move record.
|
|
|
|
boards_seen: HashSet<u64>,
|
2025-05-27 11:59:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Position {
|
2025-06-05 08:21:32 -07:00
|
|
|
pub fn empty(zobrist: Option<Arc<ZobristState>>) -> Self {
|
|
|
|
Self::new(Board::empty(zobrist))
|
2025-05-27 11:59:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Return a starting position.
|
2025-06-05 08:21:32 -07:00
|
|
|
pub fn starting(zobrist: Option<Arc<ZobristState>>) -> Self {
|
|
|
|
Self::new(Board::starting(zobrist))
|
2025-05-27 11:59:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2025-06-02 15:54:00 -07:00
|
|
|
) -> Result<Option<Piece>, PlacePieceError> {
|
2025-05-27 11:59:42 -07:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-28 16:25:55 -07:00
|
|
|
impl Position {
|
|
|
|
pub fn all_moves(&self, color: Option<Color>) -> AllPiecesMoveGenerator {
|
|
|
|
AllPiecesMoveGenerator::new(&self.board, color)
|
|
|
|
}
|
|
|
|
|
2025-06-06 21:45:07 -07:00
|
|
|
/// 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();
|
|
|
|
|
|
|
|
println!("{:?} from:{} to:{}", ply, ply.origin(), ply.target());
|
|
|
|
|
|
|
|
let ply: Move = ply.clone().into();
|
|
|
|
let record = test_board
|
|
|
|
.make_move(ply, ValidateMove::No)
|
|
|
|
.expect("unable to make generated move");
|
|
|
|
|
|
|
|
let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move));
|
|
|
|
|
|
|
|
test_board
|
|
|
|
.unmake_move(&record)
|
|
|
|
.expect("unable to unmake generated move");
|
|
|
|
|
|
|
|
move_is_legal
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2025-05-28 16:25:55 -07:00
|
|
|
#[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))),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-27 11:59:42 -07:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-01 19:02:53 -07:00
|
|
|
impl Position {
|
|
|
|
/// Make a move on the board and record it in the move list.
|
|
|
|
///
|
|
|
|
/// ## Errors
|
|
|
|
///
|
|
|
|
/// Returns one of [`MakeMoveError`] if the move cannot be made.
|
|
|
|
///
|
|
|
|
pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> {
|
|
|
|
let record = self.board.make_move(ply, validate)?;
|
|
|
|
|
|
|
|
if let Some(captured_piece) = record.captured_piece {
|
|
|
|
self.captures.push(record.color, captured_piece);
|
|
|
|
}
|
|
|
|
|
2025-06-07 08:55:34 -07:00
|
|
|
if let Some(hash) = self.board.zobrist_hash() {
|
|
|
|
// TODO: If the hash already exists here, it's a duplicate position
|
|
|
|
// and this move results in a draw. Find a way to indicate that,
|
|
|
|
// in either Board or Position.
|
|
|
|
self.boards_seen.insert(hash);
|
|
|
|
}
|
|
|
|
|
2025-06-01 19:02:53 -07:00
|
|
|
self.moves.push(record.clone());
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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)?;
|
|
|
|
|
2025-06-07 08:55:34 -07:00
|
|
|
let hash_before_unmake = self.board.zobrist_hash();
|
|
|
|
|
2025-06-01 19:02:53 -07:00
|
|
|
let unmake_result = self.board.unmake_move(&last_move_record);
|
|
|
|
|
2025-06-07 08:53:42 -07:00
|
|
|
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);
|
|
|
|
}
|
2025-06-07 08:55:34 -07:00
|
|
|
|
|
|
|
if let Some(hash_before_unmake) = hash_before_unmake {
|
|
|
|
self.boards_seen.remove(&hash_before_unmake);
|
|
|
|
}
|
2025-06-07 08:53:42 -07:00
|
|
|
} else {
|
2025-06-01 19:02:53 -07:00
|
|
|
self.moves.push(last_move_record);
|
|
|
|
}
|
|
|
|
|
|
|
|
unmake_result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-07 08:50:20 -07:00
|
|
|
impl Position {
|
|
|
|
#[must_use]
|
|
|
|
pub fn zobrist_hash(&self) -> Option<u64> {
|
|
|
|
self.board.zobrist_hash()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-27 11:59:42 -07:00
|
|
|
impl Position {
|
|
|
|
pub fn display(&self) -> DiagramFormatter {
|
|
|
|
self.board.display()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)));
|
|
|
|
}
|
|
|
|
}
|