[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>
use crate::{r#move::Castle, Position};
use chessfriend_core::{Color, File, Piece, PlacedPiece, Rank, Square};
use crate::{r#move::Castle, Position, PositionBuilder};
use chessfriend_core::{piece, Color, File, Piece, PlacedPiece, Rank, Square};
use std::fmt::Write;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -9,11 +9,19 @@ pub enum ToFenError {
FmtError(std::fmt::Error),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FromFenError;
pub trait ToFen {
type 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 {
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)]
mod tests {
use crate::test_position;
use super::*;
#[test]
fn starting_position() {
let pos = Position::starting();
println!("{pos:#?}");
let pos = test_position!(starting);
assert_eq!(
pos.to_fen(),
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,
}
#[derive(Debug)]
pub(crate) struct Squares {
pub king: Square,
pub rook: Square,

View file

@ -13,7 +13,8 @@ pub struct Builder {
player_to_move: Color,
flags: Flags,
pieces: BTreeMap<Square, Piece>,
kings: [Square; 2],
kings: [Option<Square>; 2],
en_passant_square: Option<Square>,
ply_counter: u16,
move_number: u16,
}
@ -23,6 +24,18 @@ impl Builder {
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 {
let pieces = BTreeMap::from_iter(
position
@ -38,7 +51,8 @@ impl Builder {
player_to_move: position.player_to_move(),
flags: position.flags(),
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(),
move_number: position.move_number(),
}
@ -59,6 +73,11 @@ impl Builder {
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 {
let square = piece.square();
let shape = piece.shape();
@ -66,8 +85,11 @@ impl Builder {
if shape == Shape::King {
let color = piece.color();
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());
@ -75,6 +97,17 @@ impl Builder {
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 {
let pieces = PieceBitBoards::from_iter(
self.pieces
@ -93,7 +126,7 @@ impl Builder {
.get(&starting_squares.rook)
.is_some_and(|piece| piece.shape() == Shape::Rook);
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 {
flags.clear_player_has_right_to_castle_flag(color, castle);
@ -105,7 +138,7 @@ impl Builder {
self.player_to_move,
flags,
pieces,
None,
self.en_passant_square,
self.ply_counter,
self.move_number,
)
@ -139,7 +172,8 @@ impl Default for Builder {
player_to_move: Color::White,
flags: Flags::default(),
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,
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) {
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 {

View file

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