[position] Implement FromFen for Position, Piece, and Color

I can now create Positions from FEN strings!
This commit is contained in:
Eryn Wells 2024-01-29 16:10:08 -08:00
parent 3239f288d7
commit 52b19b87d8
5 changed files with 187 additions and 13 deletions

View file

@ -1,7 +1,7 @@
// Eryn Wells <eryn@erynwells.me> // Eryn Wells <eryn@erynwells.me>
use crate::{r#move::Castle, Position}; use crate::{r#move::Castle, Position, PositionBuilder};
use chessfriend_core::{Color, File, Piece, PlacedPiece, Rank, Square}; use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square};
use std::fmt::Write; use std::fmt::Write;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -9,11 +9,19 @@ pub enum ToFenError {
FmtError(std::fmt::Error), FmtError(std::fmt::Error),
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FromFenError;
pub trait ToFen { pub trait ToFen {
type Error; type Error;
fn to_fen(&self) -> Result<String, Self::Error>; fn to_fen(&self) -> Result<String, Self::Error>;
} }
pub trait FromFen: Sized {
type Error;
fn from_fen_str(string: &str) -> Result<Self, Self::Error>;
}
impl ToFen for Position { impl ToFen for Position {
type Error = ToFenError; type Error = ToFenError;
@ -127,14 +135,131 @@ impl ToFen for PlacedPiece {
} }
} }
impl FromFen for Position {
type Error = FromFenError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
let mut builder = PositionBuilder::empty();
let mut fields = string.split(" ");
let placements = fields.next().ok_or(FromFenError)?;
let ranks = placements.split("/");
for (rank, pieces) in Rank::ALL.iter().rev().zip(ranks) {
let mut files = File::ALL.iter();
for ch in pieces.chars() {
if let Some(skip) = ch.to_digit(10) {
// TODO: Use advance_by() when it's available.
for _ in 0..skip {
files.next();
}
continue;
}
let file = files.next().ok_or(FromFenError)?;
let piece = Piece::from_fen_str(&ch.to_string())?;
builder.place_piece(PlacedPiece::new(
piece,
Square::from_file_rank(*file, *rank),
));
}
debug_assert_eq!(files.next(), None);
}
let player_to_move = Color::from_fen_str(fields.next().ok_or(FromFenError)?)?;
builder.to_move(player_to_move);
let castling_rights = fields.next().ok_or(FromFenError)?;
if castling_rights == "-" {
builder.no_castling_rights();
} else {
for ch in castling_rights.chars() {
match ch {
'K' => builder.player_can_castle(Color::White, Castle::KingSide),
'Q' => builder.player_can_castle(Color::White, Castle::QueenSide),
'k' => builder.player_can_castle(Color::Black, Castle::KingSide),
'q' => builder.player_can_castle(Color::Black, Castle::QueenSide),
_ => return Err(FromFenError),
};
}
}
let en_passant_square = fields.next().ok_or(FromFenError)?;
if en_passant_square != "-" {
let square = Square::from_algebraic_str(en_passant_square).map_err(|_| FromFenError)?;
builder.en_passant_square(Some(square));
}
let half_move_clock = fields.next().ok_or(FromFenError)?;
let half_move_clock: u16 = half_move_clock.parse().map_err(|_| FromFenError)?;
builder.ply_counter(half_move_clock);
let full_move_counter = fields.next().ok_or(FromFenError)?;
let full_move_counter: u16 = full_move_counter.parse().map_err(|_| FromFenError)?;
builder.move_number(full_move_counter);
debug_assert_eq!(fields.next(), None);
Ok(builder.build())
}
}
impl FromFen for Color {
type Error = FromFenError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
if string.len() != 1 {
return Err(FromFenError);
}
match string.chars().take(1).next().unwrap() {
'w' => Ok(Color::White),
'b' => Ok(Color::Black),
_ => Err(FromFenError),
}
}
}
impl FromFen for Piece {
type Error = FromFenError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
if string.len() != 1 {
return Err(FromFenError);
}
match string.chars().take(1).next().unwrap() {
'P' => Ok(piece!(White Pawn)),
'N' => Ok(piece!(White Knight)),
'B' => Ok(piece!(White Bishop)),
'R' => Ok(piece!(White Rook)),
'Q' => Ok(piece!(White Queen)),
'K' => Ok(piece!(White King)),
'p' => Ok(piece!(Black Pawn)),
'n' => Ok(piece!(Black Knight)),
'b' => Ok(piece!(Black Bishop)),
'r' => Ok(piece!(Black Rook)),
'q' => Ok(piece!(Black Queen)),
'k' => Ok(piece!(Black King)),
_ => Err(FromFenError),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::test_position;
use super::*; use super::*;
#[test] #[test]
fn starting_position() { fn starting_position() {
let pos = Position::starting(); let pos = test_position!(starting);
println!("{pos:#?}");
assert_eq!( assert_eq!(
pos.to_fen(), pos.to_fen(),
Ok(String::from( Ok(String::from(
@ -142,4 +267,14 @@ mod tests {
)) ))
); );
} }
#[test]
fn from_starting_fen() {
let pos =
Position::from_fen_str("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
.unwrap();
let expected = Position::starting();
assert_eq!(pos, expected, "{pos:#?}\n{expected:#?}");
}
} }

View file

@ -31,6 +31,7 @@ mod castle {
check_squares: BitBoard, check_squares: BitBoard,
} }
#[derive(Debug)]
pub(crate) struct Squares { pub(crate) struct Squares {
pub king: Square, pub king: Square,
pub rook: Square, pub rook: Square,

View file

@ -13,7 +13,8 @@ pub struct Builder {
player_to_move: Color, player_to_move: Color,
flags: Flags, flags: Flags,
pieces: BTreeMap<Square, Piece>, pieces: BTreeMap<Square, Piece>,
kings: [Square; 2], kings: [Option<Square>; 2],
en_passant_square: Option<Square>,
ply_counter: u16, ply_counter: u16,
move_number: u16, move_number: u16,
} }
@ -23,6 +24,18 @@ impl Builder {
Self::default() Self::default()
} }
pub(crate) fn empty() -> Self {
Self {
player_to_move: Color::default(),
flags: Flags::default(),
pieces: BTreeMap::default(),
kings: [None, None],
en_passant_square: None,
ply_counter: 0,
move_number: 1,
}
}
pub fn from_position(position: &Position) -> Self { pub fn from_position(position: &Position) -> Self {
let pieces = BTreeMap::from_iter( let pieces = BTreeMap::from_iter(
position position
@ -38,7 +51,8 @@ impl Builder {
player_to_move: position.player_to_move(), player_to_move: position.player_to_move(),
flags: position.flags(), flags: position.flags(),
pieces, pieces,
kings: [white_king, black_king], kings: [Some(white_king), Some(black_king)],
en_passant_square: position.en_passant_square(),
ply_counter: position.ply_counter(), ply_counter: position.ply_counter(),
move_number: position.move_number(), move_number: position.move_number(),
} }
@ -59,6 +73,11 @@ impl Builder {
self self
} }
pub fn en_passant_square(&mut self, square: Option<Square>) -> &mut Self {
self.en_passant_square = square;
self
}
pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self { pub fn place_piece(&mut self, piece: PlacedPiece) -> &mut Self {
let square = piece.square(); let square = piece.square();
let shape = piece.shape(); let shape = piece.shape();
@ -66,8 +85,11 @@ impl Builder {
if shape == Shape::King { if shape == Shape::King {
let color = piece.color(); let color = piece.color();
let color_index: usize = color as usize; let color_index: usize = color as usize;
self.pieces.remove(&self.kings[color_index]);
self.kings[color_index] = square; if let Some(king_square) = self.kings[color_index] {
self.pieces.remove(&king_square);
}
self.kings[color_index] = Some(square);
} }
self.pieces.insert(square, *piece.piece()); self.pieces.insert(square, *piece.piece());
@ -75,6 +97,17 @@ impl Builder {
self self
} }
pub fn player_can_castle(&mut self, color: Color, castle: Castle) -> &mut Self {
self.flags
.set_player_has_right_to_castle_flag(color, castle);
self
}
pub fn no_castling_rights(&mut self) -> &mut Self {
self.flags.clear_all_castling_rights();
self
}
pub fn build(&self) -> Position { pub fn build(&self) -> Position {
let pieces = PieceBitBoards::from_iter( let pieces = PieceBitBoards::from_iter(
self.pieces self.pieces
@ -93,7 +126,7 @@ impl Builder {
.get(&starting_squares.rook) .get(&starting_squares.rook)
.is_some_and(|piece| piece.shape() == Shape::Rook); .is_some_and(|piece| piece.shape() == Shape::Rook);
let king_is_on_starting_square = let king_is_on_starting_square =
self.kings[color as usize] == starting_squares.king; self.kings[color as usize] == Some(starting_squares.king);
if !king_is_on_starting_square || !has_rook_on_starting_square { if !king_is_on_starting_square || !has_rook_on_starting_square {
flags.clear_player_has_right_to_castle_flag(color, castle); flags.clear_player_has_right_to_castle_flag(color, castle);
@ -105,7 +138,7 @@ impl Builder {
self.player_to_move, self.player_to_move,
flags, flags,
pieces, pieces,
None, self.en_passant_square,
self.ply_counter, self.ply_counter,
self.move_number, self.move_number,
) )
@ -139,7 +172,8 @@ impl Default for Builder {
player_to_move: Color::White, player_to_move: Color::White,
flags: Flags::default(), flags: Flags::default(),
pieces: pieces, pieces: pieces,
kings: [white_king_square, black_king_square], kings: [Some(white_king_square), Some(black_king_square)],
en_passant_square: None,
ply_counter: 0, ply_counter: 0,
move_number: 1, move_number: 1,
} }

View file

@ -24,6 +24,10 @@ impl Flags {
pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) { pub(super) fn clear_player_has_right_to_castle_flag(&mut self, color: Color, castle: Castle) {
self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, castle)); self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, castle));
} }
pub(super) fn clear_all_castling_rights(&mut self) {
self.0 &= 0b11111100;
}
} }
impl fmt::Debug for Flags { impl fmt::Debug for Flags {

View file

@ -269,8 +269,8 @@ impl Position {
pieces, pieces,
sight: [OnceCell::new(), OnceCell::new()], sight: [OnceCell::new(), OnceCell::new()],
moves: OnceCell::new(), moves: OnceCell::new(),
half_move_counter: 0, half_move_counter,
full_move_number: 1, full_move_number,
} }
} }