From ef6e09b0ec35597e58013aa89ba72157cf514044 Mon Sep 17 00:00:00 2001 From: Walter Smuts Date: Fri, 4 Jun 2021 08:20:48 +0200 Subject: [PATCH 1/7] Add en passant target to Board struct This is in preparation for implementing the Display trait on the Game struct. The en_passant_target field is uesd in standard FEN notation and differs from the en_passant field in the following ways: * The rank they refer to, 3rd for en_passant vs 4th en_passant_target * The en_passant_target is always set on a double pawn push while the en_passant field is only set when it matters (i.e. only when it allows for an en passant capture the next move) --- src/board.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/board.rs b/src/board.rs index ff503122..220289b4 100644 --- a/src/board.rs +++ b/src/board.rs @@ -32,6 +32,7 @@ pub struct Board { checkers: BitBoard, hash: u64, en_passant: Option, + en_passant_target: Option, } /// What is the status of this game? @@ -71,6 +72,7 @@ impl Board { checkers: EMPTY, hash: 0, en_passant: None, + en_passant_target: None, } } @@ -807,6 +809,39 @@ impl Board { self.en_passant } + /// Give me the en_passant_target square, if it exists. + /// + /// ``` + /// use chess::{Board, ChessMove, Square}; + /// + /// let move1 = ChessMove::new(Square::D2, + /// Square::D4, + /// None); + /// + /// let move2 = ChessMove::new(Square::H7, + /// Square::H5, + /// None); + /// + /// let move3 = ChessMove::new(Square::D4, + /// Square::D5, + /// None); + /// + /// let move4 = ChessMove::new(Square::E7, + /// Square::E5, + /// None); + /// + /// let board = Board::default().make_move_new(move1) + /// .make_move_new(move2) + /// .make_move_new(move3) + /// .make_move_new(move4); + /// + /// assert_eq!(board.en_passant_target(), Some(Square::E6)); + /// ``` + #[inline] + pub fn en_passant_target(self) -> Option { + self.en_passant_target + } + /// Set the en_passant square. Note: This must only be called when self.en_passant is already /// None. fn set_ep(&mut self, sq: Square) { @@ -821,6 +856,10 @@ impl Board { } } + fn set_ep_target(&mut self, sq: Option) { + self.en_passant_target = sq; + } + /// Is a particular move legal? This function is very slow, but will work on unsanitized /// input. /// @@ -904,6 +943,9 @@ impl Board { result.xor(captured, dest_bb, !self.side_to_move); } + // Reset en_passant target + result.set_ep_target(None); + #[allow(deprecated)] result.remove_their_castle_rights(CastleRights::square_to_castle_rights( !self.side_to_move, @@ -957,6 +999,7 @@ impl Board { && (dest_bb & get_pawn_dest_double_moves()) != EMPTY { result.set_ep(dest); + result.set_ep_target(Some(dest.ubackward(self.side_to_move))); result.checkers ^= get_pawn_attacks(ksq, !result.side_to_move, dest_bb); } else if Some(dest.ubackward(self.side_to_move)) == self.en_passant { result.xor( From d12551c457758af01b2ce9958c552f3bd1257c5a Mon Sep 17 00:00:00 2001 From: Walter Smuts Date: Fri, 4 Jun 2021 08:22:24 +0200 Subject: [PATCH 2/7] Add en_passant_target to BoardBuilder struct To keep the one to one correspondence between the Board and BoardBuilder structs we need to duplicate the en_passant_target definition for the board_builder struct. --- src/board_builder.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/board_builder.rs b/src/board_builder.rs index da2f6795..9611eeff 100644 --- a/src/board_builder.rs +++ b/src/board_builder.rs @@ -53,6 +53,7 @@ pub struct BoardBuilder { side_to_move: Color, castle_rights: [CastleRights; 2], en_passant: Option, + en_passant_target: Option, } impl BoardBuilder { @@ -81,6 +82,7 @@ impl BoardBuilder { side_to_move: Color::White, castle_rights: [CastleRights::NoRights, CastleRights::NoRights], en_passant: None, + en_passant_target: None, } } @@ -100,6 +102,7 @@ impl BoardBuilder { /// Color::Black, /// CastleRights::NoRights, /// CastleRights::NoRights, + /// None, /// None) /// .try_into()?; /// # Ok(()) @@ -110,12 +113,14 @@ impl BoardBuilder { white_castle_rights: CastleRights, black_castle_rights: CastleRights, en_passant: Option, + en_passant_target: Option, ) -> BoardBuilder { let mut result = BoardBuilder { pieces: [None; 64], side_to_move: side_to_move, castle_rights: [white_castle_rights, black_castle_rights], en_passant: en_passant, + en_passant_target: en_passant_target, }; for piece in pieces.into_iter() { @@ -329,7 +334,7 @@ impl fmt::Display for BoardBuilder { } write!(f, " ")?; - if let Some(sq) = self.get_en_passant() { + if let Some(sq) = self.en_passant_target { write!(f, "{}", sq)?; } else { write!(f, "-")?; @@ -496,6 +501,7 @@ impl From<&Board> for BoardBuilder { board.castle_rights(Color::White), board.castle_rights(Color::Black), board.en_passant().map(|sq| sq.get_file()), + board.en_passant_target(), ) } } From 06a528f89d5730c3a0bb04782708a39e98fbd9b1 Mon Sep 17 00:00:00 2001 From: Walter Smuts Date: Fri, 4 Jun 2021 08:29:28 +0200 Subject: [PATCH 3/7] Factor get_half_move_clock logic into separate fn In preparation for implementing the Display trait on the Game struct, we need to factor out the get_half_move_clock logic to avoid logic duplication. --- src/game.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/game.rs b/src/game.rs index bee7ae19..758b1aad 100644 --- a/src/game.rs +++ b/src/game.rs @@ -170,6 +170,35 @@ impl Game { copy } + fn get_half_move_clock(&self) -> usize { + let mut reversible_moves = 0; + let mut board = self.start_pos; + for x in self.moves.iter() { + match *x { + Action::MakeMove(m) => { + let white_castle_rights = board.castle_rights(Color::White); + let black_castle_rights = board.castle_rights(Color::Black); + if board.piece_on(m.get_source()) == Some(Piece::Pawn) { + reversible_moves = 0; + } else if board.piece_on(m.get_dest()).is_some() { + reversible_moves = 0; + } else { + reversible_moves += 1; + } + board = board.make_move_new(m); + + if board.castle_rights(Color::White) != white_castle_rights + || board.castle_rights(Color::Black) != black_castle_rights + { + reversible_moves = 0; + } + } + _ => {} + } + } + reversible_moves + } + /// Determine if a player can legally declare a draw by 3-fold repetition or 50-move rule. /// /// ``` @@ -205,7 +234,6 @@ impl Game { let mut legal_moves_per_turn: Vec<(u64, Vec)> = vec![]; let mut board = self.start_pos; - let mut reversible_moves = 0; // Loop over each move, counting the reversible_moves for draw by 50 move rule, // and filling a list of legal_moves_per_turn list for 3-fold repitition @@ -216,20 +244,16 @@ impl Game { let white_castle_rights = board.castle_rights(Color::White); let black_castle_rights = board.castle_rights(Color::Black); if board.piece_on(m.get_source()) == Some(Piece::Pawn) { - reversible_moves = 0; legal_moves_per_turn.clear(); } else if board.piece_on(m.get_dest()).is_some() { - reversible_moves = 0; legal_moves_per_turn.clear(); } else { - reversible_moves += 1; } board = board.make_move_new(m); if board.castle_rights(Color::White) != white_castle_rights || board.castle_rights(Color::Black) != black_castle_rights { - reversible_moves = 0; legal_moves_per_turn.clear(); } legal_moves_per_turn @@ -239,7 +263,7 @@ impl Game { } } - if reversible_moves >= 100 { + if self.get_half_move_clock() >= 100 { return true; } From 0c8eccb560524ce5516ec77ce7ea1720ddf96b9c Mon Sep 17 00:00:00 2001 From: Walter Smuts Date: Fri, 4 Jun 2021 08:41:32 +0200 Subject: [PATCH 4/7] Factor BoardBuilder's Display trait ...into a separate get_pseudo_fen fn. This is in preparation for implementing the Display trait on the Game struct. We'll be using the get_psuedo_fen logic in the Dispay trait implementation. Once we implement the Display trait for the Game struct we'll be using the get_pseudo_fen function in two places: * In the Display trait implementation of BoardBuilder * In the Display trait implementation of Game --- src/board_builder.rs | 85 ++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/src/board_builder.rs b/src/board_builder.rs index 9611eeff..a92361ef 100644 --- a/src/board_builder.rs +++ b/src/board_builder.rs @@ -264,83 +264,90 @@ impl BoardBuilder { self.en_passant = file; self } -} - -impl Index for BoardBuilder { - type Output = Option<(Piece, Color)>; - - fn index<'a>(&'a self, index: Square) -> &'a Self::Output { - &self.pieces[index.to_index()] - } -} -impl IndexMut for BoardBuilder { - fn index_mut<'a>(&'a mut self, index: Square) -> &'a mut Self::Output { - &mut self.pieces[index.to_index()] - } -} - -impl fmt::Display for BoardBuilder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// A FEN representaion with the last two fileds, the half move clock and the full move + /// counter, omitted. + pub(crate) fn get_psuedo_fen(&self) -> String { + let mut psuedo_fen = String::new(); let mut count = 0; for rank in ALL_RANKS.iter().rev() { for file in ALL_FILES.iter() { let square = Square::make_square(*rank, *file).to_index(); if self.pieces[square].is_some() && count != 0 { - write!(f, "{}", count)?; + psuedo_fen.push_str(count.to_string().as_str()); count = 0; } if let Some((piece, color)) = self.pieces[square] { - write!(f, "{}", piece.to_string(color))?; + psuedo_fen.push_str(piece.to_string(color).as_str()); } else { count += 1; } } if count != 0 { - write!(f, "{}", count)?; + psuedo_fen.push_str(count.to_string().as_str()); } if *rank != Rank::First { - write!(f, "/")?; + psuedo_fen.push_str("/"); } count = 0; } - write!(f, " ")?; + psuedo_fen.push_str(" "); if self.side_to_move == Color::White { - write!(f, "w ")?; + psuedo_fen.push_str("w "); } else { - write!(f, "b ")?; + psuedo_fen.push_str("b "); } - write!( - f, - "{}", - self.castle_rights[Color::White.to_index()].to_string(Color::White) - )?; - write!( - f, - "{}", - self.castle_rights[Color::Black.to_index()].to_string(Color::Black) - )?; + psuedo_fen.push_str( + self.castle_rights[Color::White.to_index()] + .to_string(Color::White) + .as_str(), + ); + psuedo_fen.push_str( + self.castle_rights[Color::Black.to_index()] + .to_string(Color::Black) + .as_str(), + ); if self.castle_rights[0] == CastleRights::NoRights && self.castle_rights[1] == CastleRights::NoRights { - write!(f, "-")?; + psuedo_fen.push_str("-"); } - write!(f, " ")?; + psuedo_fen.push_str(" "); if let Some(sq) = self.en_passant_target { - write!(f, "{}", sq)?; + psuedo_fen.push_str(sq.to_string().as_str()); } else { - write!(f, "-")?; + psuedo_fen.push_str("-"); } - write!(f, " 0 1") + psuedo_fen + } +} + +impl Index for BoardBuilder { + type Output = Option<(Piece, Color)>; + + fn index<'a>(&'a self, index: Square) -> &'a Self::Output { + &self.pieces[index.to_index()] + } +} + +impl IndexMut for BoardBuilder { + fn index_mut<'a>(&'a mut self, index: Square) -> &'a mut Self::Output { + &mut self.pieces[index.to_index()] + } +} + +impl fmt::Display for BoardBuilder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} 0 1", self.get_psuedo_fen()) } } From 25916d1d1734492ee2a47b6698d986ad4f3f0fe0 Mon Sep 17 00:00:00 2001 From: Walter Smuts Date: Fri, 4 Jun 2021 08:43:56 +0200 Subject: [PATCH 5/7] Implement Display trait for Game struct --- src/board.rs | 5 +++++ src/game.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/board.rs b/src/board.rs index 220289b4..80679ff3 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1084,6 +1084,11 @@ impl Board { pub fn checkers(&self) -> &BitBoard { &self.checkers } + + pub fn get_psuedo_fen(&self) -> String { + let board_builder: BoardBuilder = self.into(); + board_builder.get_psuedo_fen() + } } impl fmt::Display for Board { diff --git a/src/game.rs b/src/game.rs index 758b1aad..cb68e5eb 100644 --- a/src/game.rs +++ b/src/game.rs @@ -4,6 +4,7 @@ use crate::color::Color; use crate::error::Error; use crate::movegen::MoveGen; use crate::piece::Piece; +use std::fmt; use std::str::FromStr; /// Contains all actions supported within the game @@ -170,6 +171,19 @@ impl Game { copy } + fn get_full_move_counter(&self) -> usize { + let mut half_moves = 2; + for x in self.moves.iter() { + match *x { + Action::MakeMove(_) => { + half_moves += 1; + } + _ => (), + } + } + half_moves / 2 + } + fn get_half_move_clock(&self) -> usize { let mut reversible_moves = 0; let mut board = self.start_pos; @@ -451,6 +465,18 @@ impl FromStr for Game { } } +impl fmt::Display for Game { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {}", + self.current_position().get_psuedo_fen(), + self.get_half_move_clock(), + self.get_full_move_counter() + ) + } +} + #[cfg(test)] pub fn fake_pgn_parser(moves: &str) -> Game { moves From ccc1afe5d02338ed8099656f4728cc2c67e69137 Mon Sep 17 00:00:00 2001 From: Walter Smuts Date: Sun, 6 Jun 2021 12:05:09 +0200 Subject: [PATCH 6/7] Change FromStr deserialization for Game ... to consider the half move clock and full move counter when deserializing from a FEN representation. --- src/game.rs | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/game.rs b/src/game.rs index cb68e5eb..be64af78 100644 --- a/src/game.rs +++ b/src/game.rs @@ -38,6 +38,8 @@ pub enum GameResult { pub struct Game { start_pos: Board, moves: Vec, + start_half_move_clock: usize, + start_full_move_counter: usize, } impl Game { @@ -50,10 +52,7 @@ impl Game { /// assert_eq!(game.current_position(), Board::default()); /// ``` pub fn new() -> Game { - Game { - start_pos: Board::default(), - moves: vec![], - } + Game::new_with_board(Board::default()) } /// Create a new `Game` with a specific starting position. @@ -65,9 +64,19 @@ impl Game { /// assert_eq!(game.current_position(), Board::default()); /// ``` pub fn new_with_board(board: Board) -> Game { + Game::new_with_board_and_counters(board, 0, 1) + } + + fn new_with_board_and_counters( + board: Board, + start_half_move_clock: usize, + start_full_move_counter: usize, + ) -> Game { Game { start_pos: board, moves: vec![], + start_half_move_clock, + start_full_move_counter, } } @@ -172,7 +181,7 @@ impl Game { } fn get_full_move_counter(&self) -> usize { - let mut half_moves = 2; + let mut half_moves = self.start_full_move_counter * 2; for x in self.moves.iter() { match *x { Action::MakeMove(_) => { @@ -185,7 +194,7 @@ impl Game { } fn get_half_move_clock(&self) -> usize { - let mut reversible_moves = 0; + let mut reversible_moves = self.start_half_move_clock; let mut board = self.start_pos; for x in self.moves.iter() { match *x { @@ -461,7 +470,33 @@ impl FromStr for Game { type Err = Error; fn from_str(fen: &str) -> Result { - Ok(Game::new_with_board(Board::from_str(fen)?)) + let half_move_clock = fen + .split(" ") + .nth(4) + .ok_or_else(|| Error::InvalidFen { + fen: String::from(fen), + })? + .parse::() + .map_err(|_| Error::InvalidFen { + fen: String::from(fen), + })?; + + let full_move_counter = fen + .split(" ") + .nth(5) + .ok_or_else(|| Error::InvalidFen { + fen: String::from(fen), + })? + .parse::() + .map_err(|_| Error::InvalidFen { + fen: String::from(fen), + })?; + + Ok(Game::new_with_board_and_counters( + Board::from_str(fen)?, + half_move_clock, + full_move_counter, + )) } } From 4d6ae78b36c446f57b3aad5f53289fa9730d76c6 Mon Sep 17 00:00:00 2001 From: Walter Smuts Date: Tue, 9 Mar 2021 22:26:09 +0200 Subject: [PATCH 7/7] Add test to enforce FEN strings for Game The test was found as a sample on: https://www.chessprogramming.org/Forsyth-Edwards_Notation The test exercises special-cases of: * En passant moves * Full move counter * Half move clock --- src/game.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/game.rs b/src/game.rs index be64af78..752b4fb2 100644 --- a/src/game.rs +++ b/src/game.rs @@ -523,6 +523,36 @@ pub fn fake_pgn_parser(moves: &str) -> Game { }) } +#[test] +fn test_fen_string() { + use crate::square::Square; + let mut game = Game::new(); + assert_eq!( + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + format!("{}", game) + ); + game.make_move(ChessMove::new(Square::E2, Square::E4, None)); + assert_eq!( + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", + format!("{}", game) + ); + game.make_move(ChessMove::new(Square::C7, Square::C5, None)); + assert_eq!( + "rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2", + format!("{}", game) + ); + game.make_move(ChessMove::new(Square::G1, Square::F3, None)); + let final_serialized_game = format!("{}", game); + assert_eq!( + "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2", + final_serialized_game + ); + assert_eq!( + final_serialized_game, + format!("{}", Game::from_str(&final_serialized_game).unwrap()), + ); +} + #[test] pub fn test_can_declare_draw() { let game = fake_pgn_parser(