use crate::board::board::{Board, Color, Piece, PieceType}; use crate::board::game::Game; use crate::board::state::{Castle, State}; use String as FenError; pub fn from_fen(fen: &str) -> Result { 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 { 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(&Piece::new(1 << square, piece_type, color)); file += 1; }; } Ok(board) } fn side_to_move(side: &str) -> Result { 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::history::History; use super::mailbox::Mailbox; use super::zobrist::zobrist_keys; fn en_passant_square(square: &str) -> Result, FenError> { let mut sqr = square.chars(); let mut files: HashMap = 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 = 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 { halfmove .parse::() .map_err(|_| FenError::from("Invalid halfmove_clock value")) } fn fullmove_counter(fullmove: &str) -> Result { if "0".eq(fullmove) { Err(FenError::from( "Full move counter is not allowed to start with 0", )) } else { fullmove .parse::() .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")] }