353 lines
11 KiB
Rust
353 lines
11 KiB
Rust
use core::fmt;
|
|
|
|
use crate::{
|
|
bitboard::{have_common_bit, square_to_bitboard},
|
|
board::{Board, Color, Kind, Piece},
|
|
square::{coords_to_square, square_to_algebraic, Square},
|
|
state::{Castle, State},
|
|
};
|
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
|
pub enum Promote {
|
|
Knight,
|
|
Rook,
|
|
Bishop,
|
|
Queen,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
|
pub enum MoveType {
|
|
Quiet,
|
|
Capture,
|
|
DoublePush,
|
|
Promotion(Promote),
|
|
PromotionCapture(Promote),
|
|
EnPassant,
|
|
Castle,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
|
pub struct Move {
|
|
pub src: usize,
|
|
pub dst: usize,
|
|
pub move_type: MoveType,
|
|
}
|
|
|
|
impl fmt::Debug for Move {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"{}{}",
|
|
square_to_algebraic(self.src),
|
|
square_to_algebraic(self.dst)
|
|
)?;
|
|
|
|
if let MoveType::Promotion(piece) | MoveType::PromotionCapture(piece) = &self.move_type {
|
|
let promote_char = match piece {
|
|
Promote::Knight => 'n',
|
|
Promote::Bishop => 'b',
|
|
Promote::Rook => 'r',
|
|
Promote::Queen => 'q',
|
|
};
|
|
write!(f, "{}", promote_char)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Move {
|
|
pub const fn new(src: usize, dst: usize) -> Self {
|
|
Self {
|
|
src,
|
|
dst,
|
|
move_type: MoveType::Quiet,
|
|
}
|
|
}
|
|
|
|
pub const fn new_with_type(src: usize, dst: usize, move_type: MoveType) -> Self {
|
|
Self {
|
|
src,
|
|
dst,
|
|
move_type,
|
|
}
|
|
}
|
|
|
|
pub fn parse_from_str(mv: &str) -> Result<Self, String> {
|
|
if mv.len() != 4 && mv.len() != 5 {
|
|
return Err("Invalid move characters length".to_string());
|
|
}
|
|
|
|
let mut mv_chars = mv.chars();
|
|
let src_file = (mv_chars.next().unwrap() as usize) - ('a' as usize);
|
|
let src_rank = (mv_chars.next().unwrap() as usize) - ('1' as usize);
|
|
let dst_file = (mv_chars.next().unwrap() as usize) - ('a' as usize);
|
|
let dst_rank = (mv_chars.next().unwrap() as usize) - ('1' as usize);
|
|
|
|
let src = coords_to_square(src_rank, src_file);
|
|
let dst = coords_to_square(dst_rank, dst_file);
|
|
|
|
if mv.len() == 5 {
|
|
let promotion_move = match mv_chars.next().unwrap() {
|
|
'n' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Knight)),
|
|
'b' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Bishop)),
|
|
'r' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Rook)),
|
|
'q' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Queen)),
|
|
_ => return Err("Unrecongisable character in promotion piece index".to_string()),
|
|
};
|
|
|
|
return Ok(promotion_move);
|
|
}
|
|
|
|
Ok(Self::new(src, dst))
|
|
}
|
|
|
|
pub fn parse_into_str(&self) -> String {
|
|
format!("{:?}", self)
|
|
}
|
|
}
|
|
|
|
impl Board {
|
|
pub fn make_move(&mut self, mv: &Move, color: Color) {
|
|
self.update_board_state(mv, color);
|
|
}
|
|
|
|
pub fn update_board_state(&mut self, mv: &Move, color: Color) {
|
|
let (own_pieces, opponent_pieces, en_passant_square) = match color {
|
|
Color::White => (
|
|
&mut self.white_pieces,
|
|
&mut self.black_pieces,
|
|
Some(mv.src + 8),
|
|
),
|
|
Color::Black => (
|
|
&mut self.black_pieces,
|
|
&mut self.white_pieces,
|
|
Some(mv.src - 8),
|
|
),
|
|
};
|
|
|
|
Self::update_game_state(&mut self.state, mv.src, mv.dst, color);
|
|
|
|
match &mv.move_type {
|
|
MoveType::Quiet => {
|
|
Self::move_piece(mv.src, mv.dst, own_pieces);
|
|
}
|
|
MoveType::Capture => {
|
|
Self::move_piece(mv.src, mv.dst, own_pieces);
|
|
Self::remove_piece(mv.dst, opponent_pieces);
|
|
}
|
|
MoveType::EnPassant => {
|
|
Self::move_piece(mv.src, mv.dst, own_pieces);
|
|
Self::remove_pawn_enpassant(mv.dst, opponent_pieces, color);
|
|
}
|
|
MoveType::DoublePush => {
|
|
Self::move_piece(mv.src, mv.dst, own_pieces);
|
|
self.state.set_en_passant_target_square(en_passant_square);
|
|
}
|
|
MoveType::Promotion(promote) => {
|
|
Self::remove_piece(mv.src, own_pieces);
|
|
Self::promote_piece(mv.dst, own_pieces, promote);
|
|
}
|
|
MoveType::PromotionCapture(promote) => {
|
|
Self::remove_piece(mv.src, own_pieces);
|
|
Self::remove_piece(mv.dst, opponent_pieces);
|
|
Self::promote_piece(mv.dst, own_pieces, promote);
|
|
}
|
|
MoveType::Castle => {
|
|
Self::move_piece(mv.src, mv.dst, own_pieces);
|
|
Self::move_rook_castle(mv.dst, own_pieces);
|
|
self.state.set_castling_ability(color, Castle::None)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_game_state(state: &mut State, src: usize, dst: usize, color: Color) {
|
|
state.set_en_passant_target_square(None);
|
|
state.update_castling_state_quiet(src, color);
|
|
state.update_castling_state_capture(dst, Color::opponent_color(color));
|
|
state.change_side();
|
|
}
|
|
|
|
fn move_piece(src: usize, dst: usize, pieces: &mut [Piece; 6]) {
|
|
for p in pieces.iter_mut() {
|
|
if have_common_bit(p.bitboard, square_to_bitboard(src)) {
|
|
p.bitboard &= !square_to_bitboard(src);
|
|
p.bitboard |= square_to_bitboard(dst);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn move_rook_castle(king_dst: usize, pieces: &mut [Piece; 6]) {
|
|
let (rook_src, rook_dst) = match king_dst {
|
|
Square::C1 | Square::C8 => (king_dst - 2, king_dst + 1),
|
|
Square::G1 | Square::G8 => (king_dst + 1, king_dst - 1),
|
|
_ => return,
|
|
};
|
|
|
|
Self::move_piece(rook_src, rook_dst, pieces);
|
|
}
|
|
|
|
fn promote_piece(square: usize, pieces: &mut [Piece; 6], promote: &Promote) {
|
|
match promote {
|
|
Promote::Knight => pieces[Kind::Knight].bitboard |= square_to_bitboard(square),
|
|
Promote::Bishop => pieces[Kind::Bishop].bitboard |= square_to_bitboard(square),
|
|
Promote::Rook => pieces[Kind::Rook].bitboard |= square_to_bitboard(square),
|
|
Promote::Queen => pieces[Kind::Queen].bitboard |= square_to_bitboard(square),
|
|
};
|
|
}
|
|
|
|
fn remove_piece(square: usize, pieces: &mut [Piece; 6]) {
|
|
for p in pieces.iter_mut() {
|
|
if have_common_bit(p.bitboard, square_to_bitboard(square)) {
|
|
p.bitboard &= !(square_to_bitboard(square));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remove_pawn_enpassant(square: usize, pieces: &mut [Piece; 6], color: Color) {
|
|
let piece_to_remove = match color {
|
|
Color::White => square - 8,
|
|
Color::Black => square + 8,
|
|
};
|
|
|
|
Self::remove_piece(piece_to_remove, pieces);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{
|
|
board::Color,
|
|
fen::from_fen,
|
|
r#move::{Move, MoveType, Promote},
|
|
square::Square,
|
|
};
|
|
|
|
const FEN_QUIET: [&str; 2] = [
|
|
"r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R w KQkq - 0 1",
|
|
"r3k2r/2p1p1qp/2npb3/1p3p2/pP2P1pP/P1PB1Q2/3PNPP1/R3K2R b KQkq b3 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_make_move_quiet() -> Result<(), String> {
|
|
let mut game = from_fen(FEN_QUIET[0])?;
|
|
let mv = Move::new_with_type(9, 25, MoveType::DoublePush);
|
|
game.board.make_move(&mv, Color::White);
|
|
assert_eq!(game, from_fen(FEN_QUIET[1])?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
const FEN_CAPTURE: [&str; 2] = [
|
|
"r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R w KQkq - 0 1",
|
|
"r3k2r/2p1p1qp/2npb3/1p3Q2/p3P1pP/P1PB4/1P1PNPP1/R3K2R b KQkq - 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_make_move_capture() -> Result<(), String> {
|
|
let mut game = from_fen(FEN_CAPTURE[0])?;
|
|
let mv = Move::new_with_type(21, 37, MoveType::Capture);
|
|
game.board.make_move(&mv, Color::White);
|
|
assert_eq!(game, from_fen(FEN_CAPTURE[1])?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
const FEN_EN_PASSANT: [&str; 2] = [
|
|
"r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R b KQkq h3 0 1",
|
|
"r3k2r/2p1p1qp/2npb3/1p3p2/p3P3/P1PB1Q1p/1P1PNPP1/R3K2R w KQkq - 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_make_move_en_passant() -> Result<(), String> {
|
|
let mut game = from_fen(FEN_EN_PASSANT[0])?;
|
|
let mv = Move::new_with_type(30, 23, MoveType::EnPassant);
|
|
game.board.make_move(&mv, Color::Black);
|
|
assert_eq!(game, from_fen(FEN_EN_PASSANT[1])?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
const FEN_DOUBLE_PUSH: [&str; 2] = [
|
|
"rnbqkbnr/p1pppppp/8/8/1p5P/8/PPPPPPP1/RNBQKBNR w KQkq - 0 1",
|
|
"rnbqkbnr/p1pppppp/8/8/1pP4P/8/PP1PPPP1/RNBQKBNR b KQkq c3 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_make_move_double_push() -> Result<(), String> {
|
|
let mut game = from_fen(FEN_DOUBLE_PUSH[0])?;
|
|
let mv = Move::new_with_type(10, 26, MoveType::DoublePush);
|
|
game.board.make_move(&mv, Color::White);
|
|
assert_eq!(game, from_fen(FEN_DOUBLE_PUSH[1])?);
|
|
Ok(())
|
|
}
|
|
|
|
const FEN_PROMOTION: [&str; 2] = [
|
|
"8/6P1/4n2b/1p6/1Kp5/6n1/4b3/1k6 w - - 0 1",
|
|
"6Q1/8/4n2b/1p6/1Kp5/6n1/4b3/1k6 b - - 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_make_move_promotion() -> Result<(), String> {
|
|
let mut game = from_fen(FEN_PROMOTION[0])?;
|
|
let mv = Move::new_with_type(54, 62, MoveType::Promotion(Promote::Queen));
|
|
game.board.make_move(&mv, Color::White);
|
|
assert_eq!(game, from_fen(FEN_PROMOTION[1])?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
const FEN_PROMOTION_CAPTURE: [&str; 2] = [
|
|
"5n2/6P1/7b/1p6/1Kp5/6n1/4b3/1k6 w - - 0 1",
|
|
"5Q2/8/7b/1p6/1Kp5/6n1/4b3/1k6 b - - 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_make_move_promotion_capture() -> Result<(), String> {
|
|
let mut game = from_fen(FEN_PROMOTION_CAPTURE[0])?;
|
|
let mv = Move::new_with_type(54, 61, MoveType::PromotionCapture(Promote::Queen));
|
|
game.board.make_move(&mv, Color::White);
|
|
assert_eq!(game, from_fen(FEN_PROMOTION_CAPTURE[1])?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
const FEN_CASTLE: [&str; 2] = [
|
|
"r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R w KQkq - 0 1",
|
|
"r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R4RK1 b kq - 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_make_move_castle() -> Result<(), String> {
|
|
let mut game = from_fen(FEN_CASTLE[0])?;
|
|
let mv = Move::new_with_type(4, 6, MoveType::Castle);
|
|
game.board.make_move(&mv, Color::White);
|
|
assert_eq!(game, from_fen(FEN_CASTLE[1])?);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_from_str() -> Result<(), String> {
|
|
let mv_str = "a1a8";
|
|
let actual = Move::parse_from_str(&mv_str)?;
|
|
let expected = Move::new(Square::A1, Square::A8);
|
|
assert_eq!(actual, expected);
|
|
|
|
let mv_str = "b7b8q";
|
|
let actual = Move::parse_from_str(&mv_str)?;
|
|
let expected =
|
|
Move::new_with_type(Square::B7, Square::B8, MoveType::Promotion(Promote::Queen));
|
|
assert_eq!(actual, expected);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "Invalid move characters length")]
|
|
fn test_parse_from_str_panic() -> () {
|
|
Move::parse_from_str(&"a7a8qk").unwrap();
|
|
}
|
|
}
|