[board, moves, position] Move make_move routines to moves crate
Declare a MakeMove trait and export it from chessfriend_moves. Declare a BoardProvider trait that both Board and Position implement. Implement the MakeMove trait for all types that implement BoardProvider, and move all the move making code to the moves crate. This change makes it possible to make moves directly on a Board, rather than requiring a Position. The indirection of declaring and implementing the trait in the moves crate is required because chessfriend_board is a dependency of chessfriend_moves. So, it would be a layering violation for Board to implement make_move() directly. The board crate cannot link the moves crate because that would introduce a circular dependency.
This commit is contained in:
parent
ecde338602
commit
40e8e055f9
7 changed files with 202 additions and 163 deletions
|
@ -11,4 +11,4 @@ mod testing;
|
|||
|
||||
pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy};
|
||||
pub use chessfriend_moves::GeneratedMove;
|
||||
pub use position::{Position, ValidateMove};
|
||||
pub use position::Position;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod captures;
|
||||
mod make_move;
|
||||
mod unmake_move;
|
||||
|
||||
use chessfriend_moves::{
|
||||
|
@ -9,14 +8,14 @@ use chessfriend_moves::{
|
|||
AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator,
|
||||
PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator,
|
||||
},
|
||||
GeneratedMove, Move, MoveRecord,
|
||||
GeneratedMove, MakeMove, Move, MoveRecord, ValidateMove,
|
||||
};
|
||||
pub use make_move::ValidateMove;
|
||||
|
||||
use captures::CapturesList;
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_board::{
|
||||
display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy,
|
||||
display::DiagramFormatter, fen::ToFenStr, Board, BoardProvider, PlacePieceError,
|
||||
PlacePieceStrategy,
|
||||
};
|
||||
use chessfriend_core::{Color, Piece, Shape, Square};
|
||||
use std::fmt;
|
||||
|
@ -142,6 +141,16 @@ impl ToFenStr for Position {
|
|||
}
|
||||
}
|
||||
|
||||
impl BoardProvider for Position {
|
||||
fn board(&self) -> &Board {
|
||||
&self.board
|
||||
}
|
||||
|
||||
fn board_mut(&mut self) -> &mut Board {
|
||||
&mut self.board
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Position {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.board == other.board
|
||||
|
|
|
@ -1,525 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::Position;
|
||||
use chessfriend_board::{
|
||||
castle::CastleEvaluationError, movement::Movement, Board, PlacePieceError, PlacePieceStrategy,
|
||||
};
|
||||
use chessfriend_core::{Color, Piece, Rank, Square, Wing};
|
||||
use chessfriend_moves::{Move, MoveRecord};
|
||||
use thiserror::Error;
|
||||
|
||||
type MakeMoveResult = Result<MoveRecord, MakeMoveError>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum ValidateMove {
|
||||
#[default]
|
||||
No,
|
||||
Yes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Eq, PartialEq)]
|
||||
pub enum MakeMoveError {
|
||||
#[error("no piece on {0}")]
|
||||
NoPiece(Square),
|
||||
|
||||
#[error("{piece} on {square} is not of active color")]
|
||||
NonActiveColor { piece: Piece, square: Square },
|
||||
|
||||
#[error("{0} cannot make move")]
|
||||
InvalidPiece(Piece),
|
||||
|
||||
#[error("cannot capture piece on {0}")]
|
||||
InvalidCapture(Square),
|
||||
|
||||
#[error("cannot capture en passant on {0}")]
|
||||
InvalidEnPassantCapture(Square),
|
||||
|
||||
#[error("no capture square")]
|
||||
NoCaptureSquare,
|
||||
|
||||
#[error("no piece to capture on {0}")]
|
||||
NoCapturePiece(Square),
|
||||
|
||||
#[error("{piece} on {origin} cannot move to {target}")]
|
||||
NoMove {
|
||||
piece: Piece,
|
||||
origin: Square,
|
||||
target: Square,
|
||||
},
|
||||
|
||||
#[error("{0}")]
|
||||
PlacePieceError(#[from] PlacePieceError),
|
||||
|
||||
#[error("{0}")]
|
||||
CastleError(#[from] CastleEvaluationError),
|
||||
|
||||
#[error("cannot promote on {0}")]
|
||||
InvalidPromotion(Square),
|
||||
|
||||
#[error("move to {0} requires promotion")]
|
||||
PromotionRequired(Square),
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Make a move in the position.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If `validate` is [`ValidateMove::Yes`], perform validation of move correctness prior to
|
||||
/// applying the move. See [`Position::validate_move`].
|
||||
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 {
|
||||
if ply.is_quiet() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_quiet_move(ply);
|
||||
}
|
||||
|
||||
if ply.is_double_push() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_double_push_move(ply);
|
||||
}
|
||||
|
||||
if ply.is_capture() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_capture_move(ply);
|
||||
}
|
||||
|
||||
if let Some(wing) = ply.castle_wing() {
|
||||
return self.make_castle_move(ply, wing);
|
||||
}
|
||||
|
||||
if ply.is_promotion() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_promotion_move(ply);
|
||||
}
|
||||
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let origin = ply.origin_square();
|
||||
|
||||
let piece = self
|
||||
.get_piece(origin)
|
||||
.ok_or(MakeMoveError::NoPiece(origin))?;
|
||||
|
||||
let target = ply.target_square();
|
||||
self.place_piece_for_move(piece, target)?;
|
||||
|
||||
self.remove_piece(origin);
|
||||
|
||||
let record = self.register_move_record(ply, None);
|
||||
|
||||
self.advance_clocks(HalfMoveClock::Advance);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let origin = ply.origin_square();
|
||||
let piece = self
|
||||
.board
|
||||
.remove_piece(origin)
|
||||
.ok_or(MakeMoveError::NoPiece(origin))?;
|
||||
|
||||
let target = ply.target_square();
|
||||
self.place_piece_for_move(piece, target)?;
|
||||
|
||||
self.board.en_passant_target = match target.rank() {
|
||||
Rank::FOUR => Some(Square::from_file_rank(target.file(), Rank::THREE)),
|
||||
Rank::FIVE => Some(Square::from_file_rank(target.file(), Rank::SIX)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let record = self.register_move_record(ply, None);
|
||||
|
||||
self.advance_clocks(HalfMoveClock::Advance);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let origin_square = ply.origin_square();
|
||||
let target_square = ply.target_square();
|
||||
|
||||
let piece = self.get_piece_for_move(origin_square)?;
|
||||
|
||||
if ply.is_en_passant() {
|
||||
let en_passant_square = self
|
||||
.board
|
||||
.en_passant_target
|
||||
.ok_or(MakeMoveError::NoCaptureSquare)?;
|
||||
if target_square != en_passant_square {
|
||||
return Err(MakeMoveError::InvalidEnPassantCapture(target_square));
|
||||
}
|
||||
}
|
||||
|
||||
let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?;
|
||||
let captured_piece = self
|
||||
.remove_piece(capture_square)
|
||||
.ok_or(MakeMoveError::NoCapturePiece(capture_square))?;
|
||||
|
||||
// Register the capture
|
||||
self.captures.push(piece.color, captured_piece);
|
||||
|
||||
self.remove_piece(origin_square).unwrap();
|
||||
|
||||
if let Some(promotion_shape) = ply.promotion_shape() {
|
||||
let promoted_piece = Piece::new(piece.color, promotion_shape);
|
||||
self.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?;
|
||||
} else {
|
||||
self.place_piece(piece, target_square, PlacePieceStrategy::Replace)?;
|
||||
}
|
||||
|
||||
let record = self.register_move_record(ply, Some(captured_piece));
|
||||
|
||||
self.advance_clocks(HalfMoveClock::Reset);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult {
|
||||
self.board.color_can_castle(wing, None)?;
|
||||
|
||||
let active_color = self.board.active_color;
|
||||
let parameters = Board::castling_parameters(wing, active_color);
|
||||
|
||||
let king = self.board.remove_piece(parameters.origin.king).unwrap();
|
||||
self.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
|
||||
|
||||
let rook = self.board.remove_piece(parameters.origin.rook).unwrap();
|
||||
self.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
|
||||
|
||||
self.board.castling_rights.revoke(active_color, wing);
|
||||
|
||||
let record = self.register_move_record(ply, None);
|
||||
|
||||
self.advance_clocks(HalfMoveClock::Advance);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let origin = ply.origin_square();
|
||||
|
||||
let piece = self.get_piece_for_move(origin)?;
|
||||
if !piece.is_pawn() {
|
||||
return Err(MakeMoveError::InvalidPiece(piece));
|
||||
}
|
||||
|
||||
let target = ply.target_square();
|
||||
if !target.rank().is_promotable_rank() {
|
||||
return Err(MakeMoveError::InvalidPromotion(target));
|
||||
}
|
||||
|
||||
if let Some(promotion_shape) = ply.promotion_shape() {
|
||||
self.remove_piece(origin);
|
||||
let promoted_piece = Piece::new(piece.color, promotion_shape);
|
||||
self.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?;
|
||||
} else {
|
||||
unreachable!(
|
||||
"Cannot make a promotion move with a ply that has no promotion shape: {ply:?}",
|
||||
);
|
||||
}
|
||||
|
||||
let record = self.register_move_record(ply, None);
|
||||
|
||||
self.advance_clocks(HalfMoveClock::Reset);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn register_move_record(&mut self, ply: Move, capture: Option<Piece>) -> MoveRecord {
|
||||
let record = MoveRecord::new(&self.board, ply, capture);
|
||||
self.moves.push(record.clone());
|
||||
|
||||
record
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn get_piece_for_move(&mut self, square: Square) -> Result<Piece, MakeMoveError> {
|
||||
self.get_piece(square).ok_or(MakeMoveError::NoPiece(square))
|
||||
}
|
||||
|
||||
fn place_piece_for_move(&mut self, piece: Piece, square: Square) -> Result<(), MakeMoveError> {
|
||||
if piece.is_pawn() && square.rank().is_promotable_rank() {
|
||||
return Err(MakeMoveError::PromotionRequired(square));
|
||||
}
|
||||
|
||||
self.place_piece(piece, square, PlacePieceStrategy::PreserveExisting)
|
||||
.map_err(MakeMoveError::PlacePieceError)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
enum HalfMoveClock {
|
||||
Reset,
|
||||
#[default]
|
||||
Advance,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn advance_clocks(&mut self, half_move_clock: HalfMoveClock) {
|
||||
match half_move_clock {
|
||||
HalfMoveClock::Reset => self.board.half_move_clock = 0,
|
||||
HalfMoveClock::Advance => self.board.half_move_clock += 1,
|
||||
}
|
||||
|
||||
self.board.active_color = self.board.active_color.next();
|
||||
|
||||
if self.board.active_color == Color::White {
|
||||
self.board.full_move_number += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> {
|
||||
if validate == ValidateMove::No {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let active_piece = self.validate_active_piece(ply)?;
|
||||
|
||||
let origin_square = ply.origin_square();
|
||||
let target_square = ply.target_square();
|
||||
|
||||
// Pawns can see squares they can't move to. So, calculating valid
|
||||
// squares requires a concept that includes Sight, but adds pawn pushes.
|
||||
// In ChessFriend, that concept is Movement.
|
||||
let movement = active_piece.movement(origin_square, &self.board);
|
||||
if !movement.contains(target_square) {
|
||||
return Err(MakeMoveError::NoMove {
|
||||
piece: active_piece,
|
||||
origin: origin_square,
|
||||
target: target_square,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: En Passant capture.
|
||||
|
||||
if let Some(capture_square) = ply.capture_square() {
|
||||
if let Some(captured_piece) = self.board.get_piece(capture_square) {
|
||||
if captured_piece.color == active_piece.color {
|
||||
return Err(MakeMoveError::InvalidCapture(capture_square));
|
||||
}
|
||||
} else {
|
||||
return Err(MakeMoveError::NoPiece(capture_square));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError> {
|
||||
let origin_square = ply.origin_square();
|
||||
|
||||
let active_piece = self
|
||||
.board
|
||||
.get_piece(origin_square)
|
||||
.ok_or(MakeMoveError::NoPiece(origin_square))?;
|
||||
|
||||
if active_piece.color != self.board.active_color {
|
||||
return Err(MakeMoveError::NonActiveColor {
|
||||
piece: active_piece,
|
||||
square: origin_square,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(active_piece)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test_position, ValidateMove};
|
||||
use chessfriend_core::{piece, Color, Square};
|
||||
use chessfriend_moves::{Move, PromotionShape};
|
||||
|
||||
type TestResult = Result<(), MakeMoveError>;
|
||||
|
||||
#[test]
|
||||
fn make_quiet_move() -> TestResult {
|
||||
let mut pos = test_position!(White Pawn on C2);
|
||||
|
||||
let ply = Move::quiet(Square::C2, Square::C3);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.get_piece(Square::C2), None);
|
||||
assert_eq!(pos.get_piece(Square::C3), Some(piece!(White Pawn)));
|
||||
assert_eq!(pos.board.active_color, Color::Black);
|
||||
assert_eq!(pos.board.half_move_clock, 1);
|
||||
|
||||
pos.board.active_color = Color::White;
|
||||
|
||||
let ply = Move::quiet(Square::C3, Square::C4);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.get_piece(Square::C3), None);
|
||||
assert_eq!(pos.get_piece(Square::C4), Some(piece!(White Pawn)));
|
||||
assert_eq!(pos.board.active_color, Color::Black);
|
||||
assert_eq!(pos.board.half_move_clock, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_invalid_quiet_pawn_move() {
|
||||
let mut pos = test_position!(White Pawn on C2);
|
||||
|
||||
let ply = Move::quiet(Square::C2, Square::D2);
|
||||
let result = pos.make_move(ply, ValidateMove::Yes);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(pos.get_piece(Square::C2), Some(piece!(White Pawn)));
|
||||
assert_eq!(pos.get_piece(Square::D2), None);
|
||||
assert_eq!(pos.board.active_color, Color::White);
|
||||
assert_eq!(pos.board.half_move_clock, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_capture_move() -> TestResult {
|
||||
let mut pos = test_position![
|
||||
White Bishop on C2,
|
||||
Black Rook on F5,
|
||||
];
|
||||
|
||||
let ply = Move::capture(Square::C2, Square::F5);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.get_piece(Square::C2), None);
|
||||
assert_eq!(pos.get_piece(Square::F5), Some(piece!(White Bishop)));
|
||||
assert_eq!(pos.captures.last(Color::White), Some(&piece!(Black Rook)));
|
||||
assert_eq!(pos.board.active_color, Color::Black);
|
||||
assert_eq!(pos.board.half_move_clock, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_en_passant_capture_move() -> TestResult {
|
||||
let mut pos = test_position![
|
||||
Black Pawn on F4,
|
||||
White Pawn on E2
|
||||
];
|
||||
|
||||
let ply = Move::double_push(Square::E2, Square::E4);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.get_piece(Square::E2), None);
|
||||
assert_eq!(pos.get_piece(Square::E4), Some(piece!(White Pawn)));
|
||||
assert_eq!(
|
||||
pos.board.en_passant_target,
|
||||
Some(Square::E3),
|
||||
"en passant square not set"
|
||||
);
|
||||
assert_eq!(pos.board.active_color, Color::Black);
|
||||
assert_eq!(pos.board.half_move_clock, 1);
|
||||
|
||||
let ply = Move::en_passant_capture(Square::F4, Square::E3);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.get_piece(Square::F4), None);
|
||||
assert_eq!(pos.get_piece(Square::E3), Some(piece!(Black Pawn)));
|
||||
assert_eq!(
|
||||
pos.get_piece(Square::E4),
|
||||
None,
|
||||
"capture target pawn not removed"
|
||||
);
|
||||
assert_eq!(pos.captures.last(Color::Black), Some(&piece!(White Pawn)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_last_rank_quiet_move_without_promotion() {
|
||||
let mut pos = test_position!(
|
||||
White Pawn on A7
|
||||
);
|
||||
|
||||
let ply = Move::quiet(Square::A7, Square::A8);
|
||||
let result = pos.make_move(ply, ValidateMove::Yes);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(pos.board.active_color, Color::White);
|
||||
assert_eq!(pos.get_piece(Square::A7), Some(piece!(White Pawn)));
|
||||
assert_eq!(pos.get_piece(Square::A8), None);
|
||||
assert_eq!(pos.board.half_move_clock, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_promotion_move() -> TestResult {
|
||||
let mut pos = test_position![
|
||||
Black Pawn on E7,
|
||||
White Pawn on F7,
|
||||
];
|
||||
|
||||
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.get_piece(Square::F7), None);
|
||||
assert_eq!(pos.get_piece(Square::F8), Some(piece!(White Queen)));
|
||||
assert_eq!(pos.board.active_color, Color::Black);
|
||||
assert_eq!(pos.board.half_move_clock, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_white_kingside_castle() -> TestResult {
|
||||
let mut pos = test_position![
|
||||
White Rook on H1,
|
||||
White King on E1,
|
||||
];
|
||||
|
||||
let ply = Move::castle(Wing::KingSide);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.board.active_color, Color::Black);
|
||||
assert_eq!(pos.get_piece(Square::E1), None);
|
||||
assert_eq!(pos.get_piece(Square::H1), None);
|
||||
assert_eq!(pos.get_piece(Square::G1), Some(piece!(White King)));
|
||||
assert_eq!(pos.get_piece(Square::F1), Some(piece!(White Rook)));
|
||||
assert!(!pos
|
||||
.board
|
||||
.castling_rights
|
||||
.color_has_right(Color::White, Wing::KingSide));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_white_queenside_castle() -> TestResult {
|
||||
let mut pos = test_position![
|
||||
White King on E1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
let ply = Move::castle(Wing::QueenSide);
|
||||
pos.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(pos.board.active_color, Color::Black);
|
||||
assert_eq!(pos.get_piece(Square::E1), None);
|
||||
assert_eq!(pos.get_piece(Square::A1), None);
|
||||
assert_eq!(pos.get_piece(Square::C1), Some(piece!(White King)));
|
||||
assert_eq!(pos.get_piece(Square::D1), Some(piece!(White Rook)));
|
||||
assert!(!pos
|
||||
.board
|
||||
.castling_rights
|
||||
.color_has_right(Color::White, Wing::QueenSide));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::position::MakeMoveError;
|
||||
use chessfriend_moves::BuildMoveError;
|
||||
use chessfriend_moves::{BuildMoveError, MakeMoveError};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_move_list {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue