219 lines
6.3 KiB
Rust
219 lines
6.3 KiB
Rust
use crate::board::board::{Board, Color, PieceType};
|
|
use crate::board::game::Game;
|
|
use crate::board::state::{Castle, State};
|
|
use String as FenError;
|
|
|
|
pub fn from_fen(fen: &str) -> Result<Game, FenError> {
|
|
let fen_parts: Vec<_> = fen.split_whitespace().collect();
|
|
|
|
let mut board = piece_placement(fen_parts[0])?;
|
|
|
|
let side_to_move = side_to_move(fen_parts[1])?;
|
|
|
|
let castling_ability = castling_ability(fen_parts[2])?;
|
|
|
|
let en_passant_square = en_passant_square(fen_parts[3])?;
|
|
|
|
let halfmove_clock = halfmove_clock(fen_parts[4])?;
|
|
|
|
let fullmove_counter = fullmove_counter(fen_parts[5])?;
|
|
|
|
board.set_state(State::load_state(
|
|
side_to_move,
|
|
castling_ability,
|
|
en_passant_square,
|
|
halfmove_clock,
|
|
fullmove_counter,
|
|
));
|
|
|
|
let mailbox = Mailbox::from_board(&board);
|
|
let hash = zobrist_keys().calculate_hash(&board);
|
|
|
|
Ok(Game {
|
|
board,
|
|
history: History::new(),
|
|
mailbox,
|
|
hash,
|
|
})
|
|
}
|
|
|
|
pub fn piece_placement(pieces: &str) -> Result<Board, FenError> {
|
|
let mut board = Board::empty_board();
|
|
let (mut file, mut rank): (u8, u8) = (0, 7);
|
|
|
|
for c in pieces.chars() {
|
|
let square = rank * 8 + file;
|
|
let color = if c.is_ascii_lowercase() {
|
|
Color::Black
|
|
} else {
|
|
Color::White
|
|
};
|
|
|
|
if let Some(piece_type) = match c.to_ascii_lowercase() {
|
|
'r' => Some(PieceType::Rook),
|
|
'n' => Some(PieceType::Knight),
|
|
'b' => Some(PieceType::Bishop),
|
|
'q' => Some(PieceType::Queen),
|
|
'k' => Some(PieceType::King),
|
|
'p' => Some(PieceType::Pawn),
|
|
'/' => {
|
|
file = 0;
|
|
rank = rank.saturating_sub(1);
|
|
continue;
|
|
}
|
|
n if n.is_numeric() => {
|
|
file += n.to_digit(10).unwrap_or(0) as u8;
|
|
None
|
|
}
|
|
_ => {
|
|
return Err(FenError::from(
|
|
"Error while parsing board piece state from FEN",
|
|
))
|
|
}
|
|
} {
|
|
board.set_piece(square_to_bitboard(square as usize), piece_type, color);
|
|
file += 1;
|
|
};
|
|
}
|
|
Ok(board)
|
|
}
|
|
|
|
fn side_to_move(side: &str) -> Result<Color, FenError> {
|
|
match side {
|
|
"w" => Ok(Color::White),
|
|
"b" => Ok(Color::Black),
|
|
_ => Err(FenError::from(
|
|
"Found invalid character while parsing in side_to_move",
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn castling_ability(castling: &str) -> Result<[Castle; 2], FenError> {
|
|
let mut bitflag = 0b0;
|
|
for c in castling.chars() {
|
|
match c {
|
|
'K' => bitflag |= 0b1000,
|
|
'Q' => bitflag |= 0b100,
|
|
'k' => bitflag |= 0b10,
|
|
'q' => bitflag |= 0b1,
|
|
'-' => break,
|
|
_ => {
|
|
return Err(FenError::from(
|
|
"Found invalid character while parsing in castling_ability",
|
|
))
|
|
}
|
|
};
|
|
}
|
|
|
|
let mut castling_ability: [Castle; 2] = [Castle::None, Castle::None];
|
|
|
|
let white_king_and_queen = (bitflag >> 2) & 0b11 == 0b11;
|
|
let white_king = (bitflag >> 3) & 1 == 1;
|
|
let white_queen = (bitflag >> 2) & 1 == 1;
|
|
castling_ability[0] = match (white_king_and_queen, white_king, white_queen) {
|
|
(true, _, _) => Castle::Both,
|
|
(_, true, _) => Castle::Short,
|
|
(_, _, true) => Castle::Long,
|
|
_ => Castle::None,
|
|
};
|
|
|
|
let black_king_and_queen = bitflag & 0b11 == 0b11;
|
|
let black_king = (bitflag >> 1) & 1 == 1;
|
|
let black_queen = bitflag & 1 == 1;
|
|
castling_ability[1] = match (black_king_and_queen, black_king, black_queen) {
|
|
(true, _, _) => Castle::Both,
|
|
(_, true, _) => Castle::Short,
|
|
(_, _, true) => Castle::Long,
|
|
_ => Castle::None,
|
|
};
|
|
|
|
Ok(castling_ability)
|
|
}
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use super::bitboard::square_to_bitboard;
|
|
use super::history::History;
|
|
use super::mailbox::Mailbox;
|
|
use super::zobrist::zobrist_keys;
|
|
|
|
fn en_passant_square(square: &str) -> Result<Option<usize>, FenError> {
|
|
let mut sqr = square.chars();
|
|
|
|
let mut files: HashMap<char, usize> = HashMap::new();
|
|
files.insert('a', 0);
|
|
files.insert('b', 1);
|
|
files.insert('c', 2);
|
|
files.insert('d', 3);
|
|
files.insert('e', 4);
|
|
files.insert('f', 5);
|
|
files.insert('g', 6);
|
|
files.insert('h', 7);
|
|
|
|
let mut ranks: HashMap<char, usize> = HashMap::new();
|
|
ranks.insert('3', 2);
|
|
ranks.insert('6', 5);
|
|
|
|
match sqr.next() {
|
|
Some(file) if files.contains_key(&file) => match sqr.next() {
|
|
Some(rank) if ranks.contains_key(&rank) => Ok(Some(
|
|
ranks.get(&rank).expect("Invalid rank") * 8
|
|
+ files.get(&file).expect("Invalid file"),
|
|
)),
|
|
Some(_) | None => Err(FenError::from(
|
|
"Not a valid rank (3 or 6) for an en passant target square",
|
|
)),
|
|
},
|
|
Some('-') => Ok(None),
|
|
Some(_) | None => Err(FenError::from("Not a file (a..h) or dash (-) character")),
|
|
}
|
|
}
|
|
|
|
fn halfmove_clock(halfmove: &str) -> Result<u8, FenError> {
|
|
halfmove
|
|
.parse::<u8>()
|
|
.map_err(|_| FenError::from("Invalid halfmove_clock value"))
|
|
}
|
|
|
|
fn fullmove_counter(fullmove: &str) -> Result<u8, FenError> {
|
|
if "0".eq(fullmove) {
|
|
Err(FenError::from(
|
|
"Full move counter is not allowed to start with 0",
|
|
))
|
|
} else {
|
|
fullmove
|
|
.parse::<u8>()
|
|
.map_err(|_| FenError::from("Invalid fullmove_counter value"))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const FEN_EXAMPLE: [&str; 5] = [
|
|
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
"rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2",
|
|
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ",
|
|
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1",
|
|
];
|
|
|
|
#[test]
|
|
fn test_from_fen() -> Result<(), String> {
|
|
let new_game = from_fen(FEN_EXAMPLE[0])?;
|
|
assert_eq!(new_game, Game::new());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn test_fen_error() -> () {
|
|
from_fen(FEN_EXAMPLE[4]).unwrap();
|
|
}
|
|
|
|
//TODO: add more happy path scenarios
|
|
//TODO: test each panic e.g. #[should_panic(expected = "less than or equal to 100")]
|
|
}
|