[position] Add move tracking to Position

Create a new MoveRecord struct that tracks the move (aka ply) and irreversible
board properties. This should make it easier to unmake moves in the future.
This commit is contained in:
Eryn Wells 2025-05-23 10:00:20 -07:00
parent 05c62dcd99
commit a9268ad194
4 changed files with 81 additions and 26 deletions

View file

@ -1,5 +1,6 @@
// Eryn Wells <eryn@erynwells.me> // Eryn Wells <eryn@erynwells.me>
mod move_record;
mod position; mod position;
#[macro_use] #[macro_use]

View file

@ -0,0 +1,15 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_board::{board::HalfMoveClock, CastleRights};
use chessfriend_core::Square;
use chessfriend_moves::Move;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct MoveRecord {
pub ply: Move,
pub en_passant_target: Option<Square>,
pub castling_rights: CastleRights,
pub half_move_clock: HalfMoveClock,
}
impl MoveRecord {}

View file

@ -1,14 +1,14 @@
// Eryn Wells <eryn@erynwells.me> // Eryn Wells <eryn@erynwells.me>
use crate::Position; use crate::{move_record::MoveRecord, Position};
use chessfriend_board::{movement::Movement, PlacePieceError, PlacePieceStrategy}; use chessfriend_board::{
castle::CastleEvaluationError, movement::Movement, PlacePieceError, PlacePieceStrategy,
};
use chessfriend_core::{Color, Piece, Rank, Square, Wing}; use chessfriend_core::{Color, Piece, Rank, Square, Wing};
use chessfriend_moves::Move; use chessfriend_moves::Move;
use thiserror::Error; use thiserror::Error;
use super::CastleEvaluationError; type MakeMoveResult = Result<MoveRecord, MakeMoveError>;
type MakeMoveResult = Result<(), MakeMoveError>;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ValidateMove { pub enum ValidateMove {
@ -69,7 +69,16 @@ impl Position {
/// ///
/// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to /// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to
/// applying the move. See [`Position::validate_move`]. /// applying the move. See [`Position::validate_move`].
pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult { pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> {
self.make_move_internal(ply, validate)?;
Ok(())
}
pub(crate) fn make_move_internal(
&mut self,
ply: Move,
validate: ValidateMove,
) -> MakeMoveResult {
self.validate_move(ply, validate)?; self.validate_move(ply, validate)?;
if ply.is_quiet() { if ply.is_quiet() {
@ -85,14 +94,14 @@ impl Position {
} }
if let Some(wing) = ply.castle_wing() { if let Some(wing) = ply.castle_wing() {
return self.make_castle_move(wing); return self.make_castle_move(ply, wing);
} }
if ply.is_promotion() { if ply.is_promotion() {
return self.make_promotion_move(ply); return self.make_promotion_move(ply);
} }
Ok(()) unreachable!();
} }
pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> { pub fn unmake_move(&mut self, ply: &Move) -> Result<(), UnmakeMoveError> {
@ -113,9 +122,11 @@ impl Position {
self.remove_piece(origin); self.remove_piece(origin);
let record = self.register_move_record(ply);
self.advance_clocks(HalfMoveClock::Advance); self.advance_clocks(HalfMoveClock::Advance);
Ok(()) Ok(record)
} }
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult { fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult {
@ -134,9 +145,11 @@ impl Position {
_ => unreachable!(), _ => unreachable!(),
}; };
let record = self.register_move_record(ply);
self.advance_clocks(HalfMoveClock::Advance); self.advance_clocks(HalfMoveClock::Advance);
Ok(()) Ok(record)
} }
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult { fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
@ -172,13 +185,15 @@ impl Position {
self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?; self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?;
} }
let record = self.register_move_record(ply);
self.advance_clocks(HalfMoveClock::Reset); self.advance_clocks(HalfMoveClock::Reset);
Ok(()) Ok(record)
} }
fn make_castle_move(&mut self, wing: Wing) -> MakeMoveResult { fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult {
self.active_color_can_castle(wing)?; self.board.active_color_can_castle(wing)?;
let active_color = self.board.active_color; let active_color = self.board.active_color;
let parameters = self.board.castling_parameters(wing); let parameters = self.board.castling_parameters(wing);
@ -191,9 +206,11 @@ impl Position {
self.board.castling_rights.revoke(active_color, wing); self.board.castling_rights.revoke(active_color, wing);
let record = self.register_move_record(ply);
self.advance_clocks(HalfMoveClock::Advance); self.advance_clocks(HalfMoveClock::Advance);
Ok(()) Ok(record)
} }
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult { fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult {
@ -219,9 +236,24 @@ impl Position {
); );
} }
let record = self.register_move_record(ply);
self.advance_clocks(HalfMoveClock::Reset); self.advance_clocks(HalfMoveClock::Reset);
Ok(()) Ok(record)
}
fn register_move_record(&mut self, ply: Move) -> MoveRecord {
let record = MoveRecord {
ply,
en_passant_target: self.board.en_passant_target,
castling_rights: self.board.castling_rights,
half_move_clock: self.board.half_move_clock,
};
self.moves.push(record.clone());
record
} }
} }
@ -230,7 +262,7 @@ impl Position {
self.get_piece(square).ok_or(MakeMoveError::NoPiece(square)) self.get_piece(square).ok_or(MakeMoveError::NoPiece(square))
} }
fn place_piece_for_move(&mut self, piece: Piece, square: Square) -> MakeMoveResult { fn place_piece_for_move(&mut self, piece: Piece, square: Square) -> Result<(), MakeMoveError> {
if piece.is_pawn() && square.rank().is_promotable_rank() { if piece.is_pawn() && square.rank().is_promotable_rank() {
return Err(MakeMoveError::PromotionRequired(square)); return Err(MakeMoveError::PromotionRequired(square));
} }
@ -326,8 +358,10 @@ mod tests {
use chessfriend_core::{piece, Color, Square}; use chessfriend_core::{piece, Color, Square};
use chessfriend_moves::{Move, PromotionShape}; use chessfriend_moves::{Move, PromotionShape};
type TestResult = Result<(), MakeMoveError>;
#[test] #[test]
fn make_quiet_move() -> MakeMoveResult { fn make_quiet_move() -> TestResult {
let mut pos = test_position!(White Pawn on C2); let mut pos = test_position!(White Pawn on C2);
let ply = Move::quiet(Square::C2, Square::C3); let ply = Move::quiet(Square::C2, Square::C3);
@ -366,23 +400,26 @@ mod tests {
} }
#[test] #[test]
fn make_capture_move() { fn make_capture_move() -> TestResult {
let mut pos = test_position![ let mut pos = test_position![
White Bishop on C2, White Bishop on C2,
Black Rook on F5, Black Rook on F5,
]; ];
let ply = Move::capture(Square::C2, Square::F5); let ply = Move::capture(Square::C2, Square::F5);
assert_eq!(pos.make_move(ply, ValidateMove::Yes), Ok(())); pos.make_move(ply, ValidateMove::Yes)?;
assert_eq!(pos.get_piece(Square::C2), None); assert_eq!(pos.get_piece(Square::C2), None);
assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop))); assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop)));
assert_eq!(pos.captures[Color::White as usize][0], piece!(Black Rook)); assert_eq!(pos.captures[Color::White as usize][0], piece!(Black Rook));
assert_eq!(pos.board.active_color, Color::Black); assert_eq!(pos.board.active_color, Color::Black);
assert_eq!(pos.board.half_move_clock, 0); assert_eq!(pos.board.half_move_clock, 0);
Ok(())
} }
#[test] #[test]
fn make_en_passant_capture_move() -> MakeMoveResult { fn make_en_passant_capture_move() -> TestResult {
let mut pos = test_position![ let mut pos = test_position![
Black Pawn on F4, Black Pawn on F4,
White Pawn on E2 White Pawn on E2
@ -433,7 +470,7 @@ mod tests {
} }
#[test] #[test]
fn make_promotion_move() -> MakeMoveResult { fn make_promotion_move() -> TestResult {
let mut pos = test_position![ let mut pos = test_position![
Black Pawn on E7, Black Pawn on E7,
White Pawn on F7, White Pawn on F7,
@ -451,7 +488,7 @@ mod tests {
} }
#[test] #[test]
fn make_white_kingside_castle() -> MakeMoveResult { fn make_white_kingside_castle() -> TestResult {
let mut pos = test_position![ let mut pos = test_position![
White Rook on H1, White Rook on H1,
White King on E1, White King on E1,
@ -474,7 +511,7 @@ mod tests {
} }
#[test] #[test]
fn make_white_queenside_castle() -> MakeMoveResult { fn make_white_queenside_castle() -> TestResult {
let mut pos = test_position![ let mut pos = test_position![
White King on E1, White King on E1,
White Rook on A1, White Rook on A1,

View file

@ -1,5 +1,6 @@
// Eryn Wells <eryn@erynwells.me> // Eryn Wells <eryn@erynwells.me>
use crate::move_record::MoveRecord;
use chessfriend_bitboard::BitBoard; use chessfriend_bitboard::BitBoard;
use chessfriend_board::{ use chessfriend_board::{
display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy, display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy,
@ -11,7 +12,8 @@ use std::{cell::OnceCell, fmt};
#[derive(Clone, Debug, Eq)] #[derive(Clone, Debug, Eq)]
pub struct Position { pub struct Position {
pub board: Board, pub board: Board,
pub(super) captures: [Vec<Piece>; Color::NUM], pub(crate) moves: Vec<MoveRecord>,
pub(crate) captures: [Vec<Piece>; Color::NUM],
} }
impl Position { impl Position {
@ -181,6 +183,7 @@ impl Default for Position {
fn default() -> Self { fn default() -> Self {
Self { Self {
board: Board::default(), board: Board::default(),
moves: Vec::default(),
captures: Default::default(), captures: Default::default(),
} }
} }
@ -202,8 +205,7 @@ impl fmt::Display for Position {
mod tests { mod tests {
use super::*; use super::*;
use crate::{test_position, Position}; use crate::{test_position, Position};
use chessfriend_bitboard::bitboard; use chessfriend_core::piece;
use chessfriend_core::{piece, Wing};
#[test] #[test]
fn piece_on_square() { fn piece_on_square() {