Compare commits
7 commits
47f532cd09
...
9ccd67ec69
Author | SHA1 | Date | |
---|---|---|---|
Bruno BELANYI | 9ccd67ec69 | ||
Bruno BELANYI | 115eb0e826 | ||
Bruno BELANYI | 555fdd0891 | ||
Bruno BELANYI | be79b99512 | ||
Bruno BELANYI | 58fe6df32d | ||
Bruno BELANYI | 5126d8ad76 | ||
Bruno BELANYI | cc520ea413 |
|
@ -9,7 +9,7 @@ pub enum InvalidError {
|
|||
InvalidPawnPosition,
|
||||
/// Castling rights do not match up with the state of the board.
|
||||
InvalidCastlingRights,
|
||||
/// En-passant target square is not empty and behind an opponent's pawn.
|
||||
/// En-passant target square is not empty, behind an opponent's pawn, on the correct rank.
|
||||
InvalidEnPassant,
|
||||
/// The two kings are next to each other.
|
||||
NeighbouringKings,
|
||||
|
@ -33,7 +33,7 @@ impl std::fmt::Display for InvalidError {
|
|||
"Castling rights do not match up with the state of the board."
|
||||
}
|
||||
Self::InvalidEnPassant => {
|
||||
"En-passant target square is not empty and behind an opponent's pawn."
|
||||
"En-passant target square is not empty, behind an opponent's pawn, on the correct rank."
|
||||
}
|
||||
Self::NeighbouringKings => "The two kings are next to each other.",
|
||||
Self::OpponentInCheck => "The opponent is currently in check.",
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
use crate::{
|
||||
fen::{FenError, FromFen},
|
||||
movegen,
|
||||
};
|
||||
use crate::movegen;
|
||||
|
||||
use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square};
|
||||
|
||||
|
@ -300,12 +297,22 @@ impl ChessBoard {
|
|||
}
|
||||
}
|
||||
|
||||
// The current en-passant target square must be empty, right behind an opponent's pawn.
|
||||
// En-passant validation
|
||||
if let Some(square) = self.en_passant() {
|
||||
// Must be empty
|
||||
if !(self.combined_occupancy() & square).is_empty() {
|
||||
return Err(InvalidError::InvalidEnPassant);
|
||||
}
|
||||
let opponent_pawns = self.occupancy(Piece::Pawn, !self.current_player());
|
||||
|
||||
let opponent = !self.current_player();
|
||||
|
||||
// Must be on the opponent's third rank
|
||||
if (square & opponent.third_rank().into_bitboard()).is_empty() {
|
||||
return Err(InvalidError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
// Must be behind a pawn
|
||||
let opponent_pawns = self.occupancy(Piece::Pawn, opponent);
|
||||
let double_pushed_pawn = self
|
||||
.current_player()
|
||||
.backward_direction()
|
||||
|
@ -403,102 +410,10 @@ impl Default for ChessBoard {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return a [ChessBoard] from the given FEN string.
|
||||
impl FromFen for ChessBoard {
|
||||
type Err = FenError;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut split = s.split_ascii_whitespace();
|
||||
|
||||
let piece_placement = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let side_to_move = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let castling_rights = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let en_passant_square = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let half_move_clock = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let full_move_counter = split.next().ok_or(FenError::InvalidFen)?;
|
||||
|
||||
let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?;
|
||||
let side = Color::from_fen(side_to_move)?;
|
||||
let en_passant = Option::<Square>::from_fen(en_passant_square)?;
|
||||
|
||||
let half_move_clock = half_move_clock
|
||||
.parse::<u8>()
|
||||
.map_err(|_| FenError::InvalidFen)?;
|
||||
let full_move_counter = full_move_counter
|
||||
.parse::<u32>()
|
||||
.map_err(|_| FenError::InvalidFen)?;
|
||||
let total_plies = (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 };
|
||||
|
||||
let (piece_occupancy, color_occupancy, combined_occupancy) = {
|
||||
let (mut pieces, mut colors, mut combined) =
|
||||
([Bitboard::EMPTY; 6], [Bitboard::EMPTY; 2], Bitboard::EMPTY);
|
||||
|
||||
let mut rank: usize = 8;
|
||||
for rank_str in piece_placement.split('/') {
|
||||
rank -= 1;
|
||||
let mut file: usize = 0;
|
||||
for c in rank_str.chars() {
|
||||
let color = if c.is_uppercase() {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
};
|
||||
let piece = match c {
|
||||
digit @ '1'..='8' => {
|
||||
// Unwrap is fine since this arm is only matched by digits
|
||||
file += digit.to_digit(10).unwrap() as usize;
|
||||
continue;
|
||||
}
|
||||
_ => Piece::from_fen(&c.to_string())?,
|
||||
};
|
||||
let (piece_board, color_board) =
|
||||
(&mut pieces[piece.index()], &mut colors[color.index()]);
|
||||
|
||||
// Only need to worry about underflow since those are `usize` values.
|
||||
if file >= 8 || rank >= 8 {
|
||||
return Err(FenError::InvalidFen);
|
||||
};
|
||||
let square = Square::new(File::from_index(file), Rank::from_index(rank));
|
||||
*piece_board |= square;
|
||||
*color_board |= square;
|
||||
combined |= square;
|
||||
file += 1;
|
||||
}
|
||||
// We haven't read exactly 8 files.
|
||||
if file != 8 {
|
||||
return Err(FenError::InvalidFen);
|
||||
}
|
||||
}
|
||||
// We haven't read exactly 8 ranks
|
||||
if rank != 0 {
|
||||
return Err(FenError::InvalidFen);
|
||||
}
|
||||
|
||||
(pieces, colors, combined)
|
||||
};
|
||||
|
||||
let res = Self {
|
||||
piece_occupancy,
|
||||
color_occupancy,
|
||||
combined_occupancy,
|
||||
castle_rights,
|
||||
en_passant,
|
||||
half_move_clock,
|
||||
total_plies,
|
||||
side,
|
||||
};
|
||||
|
||||
if let Err(err) = res.validate() {
|
||||
return Err(FenError::InvalidPosition(err));
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::board::MoveBuilder;
|
||||
use crate::fen::FromFen;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -634,238 +549,145 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn invalid_multiple_kings() {
|
||||
let position = ChessBoard {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::E1 | Square::E2 | Square::E7 | Square::E8,
|
||||
// Queen
|
||||
Bitboard::EMPTY,
|
||||
// Rook
|
||||
Bitboard::EMPTY,
|
||||
// Bishop
|
||||
Bitboard::EMPTY,
|
||||
// Knight
|
||||
Bitboard::EMPTY,
|
||||
// Pawn
|
||||
Bitboard::EMPTY,
|
||||
],
|
||||
color_occupancy: [Square::E1 | Square::E2, Square::E7 | Square::E8],
|
||||
combined_occupancy: Square::E1 | Square::E2 | Square::E7 | Square::E8,
|
||||
castle_rights: [CastleRights::NoSide; 2],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E2] = Some((Piece::King, Color::White));
|
||||
builder[Square::E7] = Some((Piece::King, Color::Black));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
InvalidError::TooManyPieces,
|
||||
);
|
||||
assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_castling_rights_no_rooks() {
|
||||
let position = ChessBoard {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::E1 | Square::E8,
|
||||
// Queen
|
||||
Bitboard::EMPTY,
|
||||
// Rook
|
||||
Bitboard::EMPTY,
|
||||
// Bishop
|
||||
Bitboard::EMPTY,
|
||||
// Knight
|
||||
Bitboard::EMPTY,
|
||||
// Pawn
|
||||
Bitboard::EMPTY,
|
||||
],
|
||||
color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()],
|
||||
combined_occupancy: Square::E1 | Square::E8,
|
||||
castle_rights: [CastleRights::BothSides; 2],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder.with_castle_rights(CastleRights::BothSides, Color::White);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
InvalidError::InvalidCastlingRights,
|
||||
);
|
||||
assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_castling_rights_moved_king() {
|
||||
let position = ChessBoard {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::E2 | Square::E7,
|
||||
// Queen
|
||||
Bitboard::EMPTY,
|
||||
// Rook
|
||||
Square::A1 | Square::A8 | Square::H1 | Square::H8,
|
||||
// Bishop
|
||||
Bitboard::EMPTY,
|
||||
// Knight
|
||||
Bitboard::EMPTY,
|
||||
// Pawn
|
||||
Bitboard::EMPTY,
|
||||
],
|
||||
color_occupancy: [
|
||||
Square::A1 | Square::E2 | Square::H1,
|
||||
Square::A8 | Square::E7 | Square::H8,
|
||||
],
|
||||
combined_occupancy: Square::A1
|
||||
| Square::A8
|
||||
| Square::E2
|
||||
| Square::E7
|
||||
| Square::H1
|
||||
| Square::H8,
|
||||
castle_rights: [CastleRights::BothSides; 2],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E2] = Some((Piece::King, Color::White));
|
||||
builder[Square::A1] = Some((Piece::Rook, Color::White));
|
||||
builder[Square::H1] = Some((Piece::Rook, Color::White));
|
||||
builder[Square::E7] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A8] = Some((Piece::Rook, Color::Black));
|
||||
builder[Square::H8] = Some((Piece::Rook, Color::Black));
|
||||
builder.with_castle_rights(CastleRights::BothSides, Color::White);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
InvalidError::InvalidCastlingRights,
|
||||
);
|
||||
assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_en_passant() {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A5] = Some((Piece::Pawn, Color::Black));
|
||||
builder.with_en_passant(Square::A6);
|
||||
TryInto::<ChessBoard>::try_into(builder).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_en_passant_not_empty() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A6] = Some((Piece::Rook, Color::Black));
|
||||
builder[Square::A5] = Some((Piece::Pawn, Color::Black));
|
||||
builder.with_en_passant(Square::A6);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_en_passant_not_behind_pawn() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A5] = Some((Piece::Rook, Color::Black));
|
||||
builder.with_en_passant(Square::A6);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_en_passant_incorrect_rank() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A4] = Some((Piece::Pawn, Color::Black));
|
||||
builder.with_en_passant(Square::A5);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_kings_next_to_each_other() {
|
||||
let position = ChessBoard {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::E2 | Square::E3,
|
||||
// Queen
|
||||
Bitboard::EMPTY,
|
||||
// Rook
|
||||
Bitboard::EMPTY,
|
||||
// Bishop
|
||||
Bitboard::EMPTY,
|
||||
// Knight
|
||||
Bitboard::EMPTY,
|
||||
// Pawn
|
||||
Bitboard::EMPTY,
|
||||
],
|
||||
color_occupancy: [Square::E2.into_bitboard(), Square::E3.into_bitboard()],
|
||||
combined_occupancy: Square::E2 | Square::E3,
|
||||
castle_rights: [CastleRights::NoSide; 2],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E2] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
InvalidError::NeighbouringKings,
|
||||
);
|
||||
assert_eq!(res.err().unwrap(), InvalidError::NeighbouringKings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_opponent_in_check() {
|
||||
let position = ChessBoard {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::E1 | Square::E8,
|
||||
// Queen
|
||||
Square::E7.into_bitboard(),
|
||||
// Rook
|
||||
Bitboard::EMPTY,
|
||||
// Bishop
|
||||
Bitboard::EMPTY,
|
||||
// Knight
|
||||
Bitboard::EMPTY,
|
||||
// Pawn
|
||||
Bitboard::EMPTY,
|
||||
],
|
||||
color_occupancy: [Square::E1 | Square::E7, Square::E8.into_bitboard()],
|
||||
combined_occupancy: Square::E1 | Square::E7 | Square::E8,
|
||||
castle_rights: [CastleRights::NoSide; 2],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E7] = Some((Piece::Queen, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
InvalidError::OpponentInCheck,
|
||||
);
|
||||
assert_eq!(res.err().unwrap(), InvalidError::OpponentInCheck);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_pawn_on_first_rank() {
|
||||
let position = ChessBoard {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::H1 | Square::H8,
|
||||
// Queen
|
||||
Bitboard::EMPTY,
|
||||
// Rook
|
||||
Bitboard::EMPTY,
|
||||
// Bishop
|
||||
Bitboard::EMPTY,
|
||||
// Knight
|
||||
Bitboard::EMPTY,
|
||||
// Pawn
|
||||
Square::A1.into_bitboard(),
|
||||
],
|
||||
color_occupancy: [Square::A1 | Square::H1, Square::H8.into_bitboard()],
|
||||
combined_occupancy: Square::A1 | Square::H1 | Square::H8,
|
||||
castle_rights: [CastleRights::NoSide; 2],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::H1] = Some((Piece::King, Color::White));
|
||||
builder[Square::A1] = Some((Piece::Pawn, Color::White));
|
||||
builder[Square::H8] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
InvalidError::InvalidPawnPosition,
|
||||
);
|
||||
assert_eq!(res.err().unwrap(), InvalidError::InvalidPawnPosition);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_too_many_pieces() {
|
||||
let position = ChessBoard {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::H1 | Square::H8,
|
||||
// Queen
|
||||
Bitboard::EMPTY,
|
||||
// Rook
|
||||
Bitboard::EMPTY,
|
||||
// Bishop
|
||||
Bitboard::EMPTY,
|
||||
// Knight
|
||||
Bitboard::EMPTY,
|
||||
// Pawn
|
||||
File::B.into_bitboard()
|
||||
| File::C.into_bitboard()
|
||||
| File::D.into_bitboard()
|
||||
| File::E.into_bitboard(),
|
||||
],
|
||||
color_occupancy: [
|
||||
File::B.into_bitboard() | File::C.into_bitboard() | Square::H1,
|
||||
File::D.into_bitboard() | File::E.into_bitboard() | Square::H8,
|
||||
],
|
||||
combined_occupancy: File::B.into_bitboard()
|
||||
| File::C.into_bitboard()
|
||||
| File::D.into_bitboard()
|
||||
| File::E.into_bitboard()
|
||||
| Square::H1
|
||||
| Square::H8,
|
||||
castle_rights: [CastleRights::NoSide; 2],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::H1] = Some((Piece::King, Color::White));
|
||||
builder[Square::H8] = Some((Piece::King, Color::Black));
|
||||
for square in (File::B.into_bitboard() | File::C.into_bitboard()) {
|
||||
builder[square] = Some((Piece::Pawn, Color::White));
|
||||
}
|
||||
for square in (File::F.into_bitboard() | File::G.into_bitboard()) {
|
||||
builder[square] = Some((Piece::Pawn, Color::Black));
|
||||
}
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
InvalidError::TooManyPieces,
|
||||
);
|
||||
assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -911,78 +733,6 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fen_default_position() {
|
||||
let default_position = ChessBoard::default();
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
||||
.unwrap(),
|
||||
default_position
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fen_en_passant() {
|
||||
// Start from default position
|
||||
let mut position = ChessBoard::default();
|
||||
position.do_move(
|
||||
MoveBuilder {
|
||||
piece: Piece::Pawn,
|
||||
start: Square::E2,
|
||||
destination: Square::E4,
|
||||
capture: None,
|
||||
promotion: None,
|
||||
en_passant: false,
|
||||
double_step: true,
|
||||
castling: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
// And now c5
|
||||
position.do_move(
|
||||
MoveBuilder {
|
||||
piece: Piece::Pawn,
|
||||
start: Square::C7,
|
||||
destination: Square::C5,
|
||||
capture: None,
|
||||
promotion: None,
|
||||
en_passant: false,
|
||||
double_step: true,
|
||||
castling: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
// Finally, Nf3
|
||||
position.do_move(
|
||||
MoveBuilder {
|
||||
piece: Piece::Knight,
|
||||
start: Square::G1,
|
||||
destination: Square::F3,
|
||||
capture: None,
|
||||
promotion: None,
|
||||
en_passant: false,
|
||||
double_step: false,
|
||||
castling: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn do_move() {
|
||||
// Start from default position
|
||||
|
|
175
src/fen.rs
175
src/fen.rs
|
@ -1,4 +1,6 @@
|
|||
use crate::board::{CastleRights, Color, File, InvalidError, Piece, Rank, Square};
|
||||
use crate::board::{
|
||||
CastleRights, ChessBoard, ChessBoardBuilder, Color, File, InvalidError, Piece, Rank, Square,
|
||||
};
|
||||
|
||||
/// A trait to mark items that can be converted from a FEN input.
|
||||
pub trait FromFen: Sized {
|
||||
|
@ -27,6 +29,13 @@ impl std::fmt::Display for FenError {
|
|||
|
||||
impl std::error::Error for FenError {}
|
||||
|
||||
/// Allow converting a [InvalidError] into [FenError], for use with the '?' operator.
|
||||
impl From<InvalidError> for FenError {
|
||||
fn from(err: InvalidError) -> Self {
|
||||
Self::InvalidPosition(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
|
||||
impl FromFen for [CastleRights; 2] {
|
||||
type Err = FenError;
|
||||
|
@ -108,3 +117,167 @@ impl FromFen for Piece {
|
|||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [ChessBoard] from the given FEN string.
|
||||
impl FromFen for ChessBoard {
|
||||
type Err = FenError;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut split = s.split_ascii_whitespace();
|
||||
|
||||
let piece_placement = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let side_to_move = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let castling_rights = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let en_passant_square = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let half_move_clock = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let full_move_counter = split.next().ok_or(FenError::InvalidFen)?;
|
||||
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
|
||||
let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?;
|
||||
for color in Color::iter() {
|
||||
builder.with_castle_rights(castle_rights[color.index()], color);
|
||||
}
|
||||
|
||||
let side = Color::from_fen(side_to_move)?;
|
||||
builder.with_current_player(side);
|
||||
|
||||
if let Some(square) = Option::<Square>::from_fen(en_passant_square)? {
|
||||
builder.with_en_passant(square);
|
||||
};
|
||||
|
||||
let half_move_clock = half_move_clock
|
||||
.parse::<u8>()
|
||||
.map_err(|_| FenError::InvalidFen)?;
|
||||
builder.with_half_move_clock(half_move_clock);
|
||||
|
||||
let full_move_counter = full_move_counter
|
||||
.parse::<u32>()
|
||||
.map_err(|_| FenError::InvalidFen)?;
|
||||
builder.with_total_plies(
|
||||
(full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 },
|
||||
);
|
||||
|
||||
{
|
||||
let mut rank: usize = 8;
|
||||
for rank_str in piece_placement.split('/') {
|
||||
rank -= 1;
|
||||
let mut file: usize = 0;
|
||||
for c in rank_str.chars() {
|
||||
let color = if c.is_uppercase() {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
};
|
||||
let piece = match c {
|
||||
digit @ '1'..='8' => {
|
||||
// Unwrap is fine since this arm is only matched by digits
|
||||
file += digit.to_digit(10).unwrap() as usize;
|
||||
continue;
|
||||
}
|
||||
_ => Piece::from_fen(&c.to_string())?,
|
||||
};
|
||||
|
||||
// Only need to worry about underflow since those are `usize` values.
|
||||
if file >= 8 || rank >= 8 {
|
||||
return Err(FenError::InvalidFen);
|
||||
};
|
||||
|
||||
let square = Square::new(File::from_index(file), Rank::from_index(rank));
|
||||
|
||||
builder[square] = Some((piece, color));
|
||||
file += 1;
|
||||
}
|
||||
// We haven't read exactly 8 files.
|
||||
if file != 8 {
|
||||
return Err(FenError::InvalidFen);
|
||||
}
|
||||
}
|
||||
// We haven't read exactly 8 ranks
|
||||
if rank != 0 {
|
||||
return Err(FenError::InvalidFen);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(builder.try_into()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::board::MoveBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_position() {
|
||||
let default_position = ChessBoard::default();
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
||||
.unwrap(),
|
||||
default_position
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn en_passant() {
|
||||
// Start from default position
|
||||
let mut position = ChessBoard::default();
|
||||
position.do_move(
|
||||
MoveBuilder {
|
||||
piece: Piece::Pawn,
|
||||
start: Square::E2,
|
||||
destination: Square::E4,
|
||||
capture: None,
|
||||
promotion: None,
|
||||
en_passant: false,
|
||||
double_step: true,
|
||||
castling: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
// And now c5
|
||||
position.do_move(
|
||||
MoveBuilder {
|
||||
piece: Piece::Pawn,
|
||||
start: Square::C7,
|
||||
destination: Square::C5,
|
||||
capture: None,
|
||||
promotion: None,
|
||||
en_passant: false,
|
||||
double_step: true,
|
||||
castling: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
// Finally, Nf3
|
||||
position.do_move(
|
||||
MoveBuilder {
|
||||
piece: Piece::Knight,
|
||||
start: Square::G1,
|
||||
destination: Square::F3,
|
||||
capture: None,
|
||||
promotion: None,
|
||||
en_passant: false,
|
||||
double_step: false,
|
||||
castling: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue