use crate::engine::common::{ChessMan, Color, Piece, Square};
use crate::engine::errors::ChessError;
use regex::Regex;
pub struct Fen {
board: [[Option<ChessMan>; 8]; 8],
en_passant: Option<Square>,
pub king_side_castle_white: bool,
pub queen_side_castle_white: bool,
pub king_side_castle_black: bool,
pub queen_side_castle_black: bool,
turn: Color,
}
impl Fen {
pub fn new_from_board(
board: [[Option<ChessMan>; 8]; 8],
turn: Color,
en_passant: Option<Square>,
king_side_castle_white: bool,
queen_side_castle_white: bool,
king_side_castle_black: bool,
queen_side_castle_black: bool,
) -> Self {
Fen {
board,
en_passant,
king_side_castle_white,
queen_side_castle_white,
king_side_castle_black,
queen_side_castle_black,
turn,
}
}
pub fn parse(data: &str) -> Result<Self, ChessError> {
if !Fen::is_valid(data) {
return Err(ChessError::ValueError("Invalid FEN string".to_string()));
}
let lineup = data.split(' ').next().unwrap();
let meta = data.split(' ').skip(1).collect::<Vec<&str>>().join(" ");
let re =
Regex::new(r"^(?P<color>[wb])\s+(?P<castle>[KQkq-]+)\s+(?P<en_passant>[a-h][36]|-)")
.unwrap();
let captures = re.captures(&meta);
if captures.is_none() {
return Err(ChessError::ValueError("Invalid FEN string".to_string()));
}
let captures = captures.unwrap();
let color = captures.name("color").unwrap().as_str();
let turn = match color {
"w" => Some(Color::White),
"b" => Some(Color::Black),
_ => {
return Err(ChessError::ValueError("Invalid FEN string".to_string()));
}
};
let castle_flags = captures
.name("castle")
.unwrap()
.as_str()
.chars()
.collect::<Vec<char>>();
let king_side_castle_white = castle_flags.contains(&'K');
let queen_side_castle_white = castle_flags.contains(&'Q');
let king_side_castle_black = castle_flags.contains(&'k');
let queen_side_castle_black = castle_flags.contains(&'q');
let mut en_passant_square: Option<Square> = None;
if let Some(capture_en_passant) = captures.name("en_passant") {
en_passant_square = Square::from_str(capture_en_passant.as_str());
}
let mut board = [[None; 8]; 8];
let mut file_idx = 0;
for (rank_idx, row) in lineup
.split('/')
.collect::<Vec<&str>>()
.iter()
.rev()
.enumerate()
{
for c in row.chars() {
if !Fen::check_boundaries(rank_idx, file_idx) {
return Err(ChessError::ValueError("out of bounds".to_string()));
}
match c {
'1' => {
file_idx += 1;
}
'2' => {
file_idx += 2;
}
'3' => {
file_idx += 3;
}
'4' => {
file_idx += 4;
}
'5' => {
file_idx += 5;
}
'6' => {
file_idx += 6;
}
'7' => {
file_idx += 7;
}
'8' => {
file_idx += 8;
}
'p' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
file_idx += 1;
}
'r' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::Black,
piece: Piece::Rook,
});
file_idx += 1;
}
'n' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::Black,
piece: Piece::Knight,
});
file_idx += 1;
}
'b' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::Black,
piece: Piece::Bishop,
});
file_idx += 1;
}
'q' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::Black,
piece: Piece::Queen,
});
file_idx += 1;
}
'k' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::Black,
piece: Piece::King,
});
file_idx += 1;
}
'P' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
file_idx += 1;
}
'R' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::White,
piece: Piece::Rook,
});
file_idx += 1;
}
'N' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::White,
piece: Piece::Knight,
});
file_idx += 1;
}
'B' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::White,
piece: Piece::Bishop,
});
file_idx += 1;
}
'Q' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::White,
piece: Piece::Queen,
});
file_idx += 1;
}
'K' => {
board[rank_idx][file_idx] = Some(ChessMan {
color: Color::White,
piece: Piece::King,
});
file_idx += 1;
}
_ => {}
}
}
file_idx = 0;
}
Ok(Self {
board,
en_passant: en_passant_square,
king_side_castle_white,
queen_side_castle_white,
king_side_castle_black,
queen_side_castle_black,
turn: turn.unwrap(),
})
}
pub fn build_string(&self) -> String {
format!(
"{} {} {} {}",
self.build_board_string(),
self.build_turn_string(),
self.build_castling_string(),
self.build_en_passant_string(),
)
}
fn build_board_string(&self) -> String {
let mut board_str = String::new();
for rank in (0..8).rev() {
let mut empty_count = 0;
for file in 0..8 {
if let Some(chess_man) = self.board[rank][file] {
if empty_count > 0 {
board_str.push_str(&empty_count.to_string());
empty_count = 0;
}
let piece_char = match chess_man.piece {
Piece::Pawn => 'P',
Piece::Knight => 'N',
Piece::Bishop => 'B',
Piece::Rook => 'R',
Piece::Queen => 'Q',
Piece::King => 'K',
};
let piece_char = if chess_man.color == Color::White {
piece_char.to_ascii_uppercase()
} else {
piece_char.to_ascii_lowercase()
};
board_str.push(piece_char);
} else {
empty_count += 1;
}
}
if empty_count > 0 {
board_str.push_str(&empty_count.to_string());
}
if rank > 0 {
board_str.push('/');
}
}
board_str
}
fn build_turn_string(&self) -> String {
match self.turn {
Color::White => "w".to_string(),
Color::Black => "b".to_string(),
}
}
fn build_castling_string(&self) -> String {
let mut castling_str = String::new();
if self.king_side_castle_white {
castling_str.push('K');
}
if self.queen_side_castle_white {
castling_str.push('Q');
}
if self.king_side_castle_black {
castling_str.push('k');
}
if self.queen_side_castle_black {
castling_str.push('q');
}
if castling_str.is_empty() {
"-".to_string()
} else {
castling_str
}
}
fn build_en_passant_string(&self) -> String {
match self.en_passant {
Some(square) => square.to_string(),
None => "-".to_string(),
}
}
fn is_valid(data: &str) -> bool {
let re = Regex::new(r"^([pnbrqkPNBRQK1-8]+/){7}[pnbrqkPNBRQK1-8]+ [wb]").unwrap();
re.is_match(data)
}
fn check_boundaries(x: usize, y: usize) -> bool {
x < 8 && y < 8
}
pub fn iter_board(&self) -> impl Iterator<Item = &Option<ChessMan>> {
self.board.iter().flatten()
}
pub fn turn(&self) -> Color {
self.turn
}
pub fn en_passant(&self) -> Option<Square> {
self.en_passant
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::common::{File, Rank};
#[test]
fn test_parse() {
let data =
String::from("r3k2r/pp1n2pp/2p2q2/b2p1n2/BP1Pp3/P1N2P2/2PB2PP/R2Q1RK1 w kq b3 0 13");
let mut expected = [[None; 8]; 8];
expected[0][0] = Some(ChessMan {
color: Color::White,
piece: Piece::Rook,
});
expected[0][3] = Some(ChessMan {
color: Color::White,
piece: Piece::Queen,
});
expected[0][5] = Some(ChessMan {
color: Color::White,
piece: Piece::Rook,
});
expected[0][6] = Some(ChessMan {
color: Color::White,
piece: Piece::King,
});
expected[1][2] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
expected[1][3] = Some(ChessMan {
color: Color::White,
piece: Piece::Bishop,
});
expected[1][6] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
expected[1][7] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
expected[2][0] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
expected[2][2] = Some(ChessMan {
color: Color::White,
piece: Piece::Knight,
});
expected[2][5] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
expected[3][0] = Some(ChessMan {
color: Color::White,
piece: Piece::Bishop,
});
expected[3][1] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
expected[3][3] = Some(ChessMan {
color: Color::White,
piece: Piece::Pawn,
});
expected[3][4] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
expected[4][0] = Some(ChessMan {
color: Color::Black,
piece: Piece::Bishop,
});
expected[4][3] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
expected[4][5] = Some(ChessMan {
color: Color::Black,
piece: Piece::Knight,
});
expected[5][2] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
expected[5][5] = Some(ChessMan {
color: Color::Black,
piece: Piece::Queen,
});
expected[6][0] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
expected[6][1] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
expected[6][3] = Some(ChessMan {
color: Color::Black,
piece: Piece::Knight,
});
expected[6][6] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
expected[6][7] = Some(ChessMan {
color: Color::Black,
piece: Piece::Pawn,
});
expected[7][0] = Some(ChessMan {
color: Color::Black,
piece: Piece::Rook,
});
expected[7][4] = Some(ChessMan {
color: Color::Black,
piece: Piece::King,
});
expected[7][7] = Some(ChessMan {
color: Color::Black,
piece: Piece::Rook,
});
let result = Fen::parse(&data).unwrap();
assert_eq!(result.board, expected);
assert_eq!(result.turn, Color::White);
}
#[test]
fn test_parse_color() {
let data = String::from("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1");
let result = Fen::parse(&data).unwrap();
assert_eq!(result.turn, Color::Black);
}
#[test]
fn test_parse_en_passant() {
let data = String::from("nbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1");
let result = Fen::parse(&data).unwrap();
assert_eq!(
result.en_passant.unwrap(),
Square::new(File::E, Rank::Three)
);
}
#[test]
fn test_parse_castling() {
let data =
String::from("r3kb1r/pp2pppp/n1pp3n/q4b2/1PB5/N3PN2/PBPP1PPP/R2QK1R1 b Qkq - 4 7");
let result = Fen::parse(&data).unwrap();
assert!(!result.king_side_castle_white);
assert!(result.queen_side_castle_white);
assert!(result.king_side_castle_black);
assert!(result.queen_side_castle_black);
}
#[test]
fn test_parse_fails() {
let data =
String::from("r3k2r/pp4n2pp/2p2q2/b2p1n2/BP1Pp3/P1N2P2/2PB2PP/R2Q1RK1 w kq b3 0 13");
let result = Fen::parse(&data);
match result {
Err(ChessError::ValueError(_)) => (),
_ => unreachable!("Expected ValueError"),
}
}
#[test]
fn test_build_fen_string() {
let data = String::from("r3k2r/pp1n2pp/2p2q2/b2p1n2/BP1Pp3/P1N2P2/2PB2PP/R2Q1RK1 w kq b3");
let fen = Fen::parse(&data).unwrap();
let result = fen.build_string();
assert_eq!(result, data)
}
#[test]
fn test_fen_is_valid() {
let data =
String::from("r3k2r/pp1n2pp/2p2q2/b2p1n2/BP1Pp3/P1N2P2/2PB2PP/R2Q1RK1 w kq b3 0 13");
assert!(Fen::is_valid(&data));
}
#[test]
fn test_fen_is_invalid() {
let data = String::from("r3k2r/");
assert!(!Fen::is_valid(&data));
}
}