diff --git a/src/fen.rs b/src/fen.rs index 0198771..9b8d6af 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,5 +1,7 @@ use crate::{BoardState, CastleRights, ColPiece, Color, Square, BOARD_HEIGHT, BOARD_WIDTH}; +pub const START_POSITION: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + pub trait FromFen { type Error; fn from_fen(_: String) -> Result diff --git a/src/lib.rs b/src/lib.rs index 7457a07..5791f76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,18 +111,19 @@ impl ColPiece { struct Square(usize); #[derive(Debug)] -enum IndexError { +enum SquareError { OutOfBounds, + InvalidCharacter(char), } impl TryFrom for Square { - type Error = IndexError; + type Error = SquareError; fn try_from(value: usize) -> Result { if (0..N_SQUARES).contains(&value) { Ok(Square(value)) } else { - Err(IndexError::OutOfBounds) + Err(SquareError::OutOfBounds) } } } @@ -132,7 +133,7 @@ impl From for usize { } } impl Square { - fn from_row_col(r: usize, c: usize) -> Result { + fn from_row_col(r: usize, c: usize) -> Result { //! Get index of square based on row and column. let ret = BOARD_WIDTH * r + c; ret.try_into() @@ -154,6 +155,27 @@ impl Square { let file = letters[col]; format!("{file}{rank}") } + + /// Convert typical human-readable form (e.g. `e4`) to square index. + fn from_algebraic(value: String) -> Result { + let bytes = value.as_bytes(); + let col = match bytes[0] as char { + 'a' => 0, + 'b' => 1, + 'c' => 2, + 'd' => 3, + 'e' => 4, + 'f' => 5, + 'g' => 6, + 'h' => 7, + _ => {return Err(SquareError::InvalidCharacter(bytes[0] as char))} + }; + if let Some(row) = (bytes[1] as char).to_digit(10) { + Square::from_row_col(row as usize - 1, col as usize) + } else { + Err(SquareError::InvalidCharacter(bytes[1] as char)) + } + } } #[cfg(test)] @@ -161,9 +183,11 @@ mod tests { use super::*; #[test] - fn test_to_algebraic() { - for (sqr, idx) in [("a1", 0), ("a8", 56), ("h1", 7), ("h8", 63)] { - assert_eq!(Square::try_from(idx).unwrap().to_algebraic(), sqr) + fn test_to_from_algebraic() { + let test_cases = [("a1", 0), ("a8", 56), ("h1", 7), ("h8", 63)]; + for (sqr, idx) in test_cases { + assert_eq!(Square::try_from(idx).unwrap().to_algebraic(), sqr); + assert_eq!(Square::from_algebraic(sqr.to_string()).unwrap(), Square::try_from(idx).unwrap()); } } } diff --git a/src/movegen.rs b/src/movegen.rs index 508c2f4..95fd0ed 100644 --- a/src/movegen.rs +++ b/src/movegen.rs @@ -1,6 +1,7 @@ //! Move generation. -use crate::{Color, Square, BoardState}; +use crate::fen::{FromFen, ToFen, START_POSITION}; +use crate::{BoardState, Color, Square, BOARD_WIDTH}; use std::rc::Rc; /// Game tree node. @@ -11,6 +12,16 @@ struct Node { prev: Option>, } +impl Default for Node { + fn default() -> Self { + Node { + pos: BoardState::from_fen(START_POSITION.to_string()) + .expect("Starting FEN should be valid"), + prev: None, + } + } +} + /// Piece enum specifically for promotions. #[derive(Debug, Copy, Clone)] enum PromotePiece { @@ -26,15 +37,17 @@ struct MoveData { dest: Square, } /// Pseudo-legal move. +/// +/// No checking is made to see if the move is actually pseudo-legal. enum Move { /// Pawn promotes to another piece. - Promotion { data: MoveData, piece: PromotePiece }, + Promotion(MoveData, PromotePiece), /// King castles with rook. - Castle { data: MoveData }, + Castle(MoveData), /// Capture, or push move. - Normal { data: MoveData }, + Normal(MoveData), /// This move is an en-passant capture. - EnPassant { data: MoveData }, + EnPassant(MoveData), } impl Move { @@ -53,16 +66,127 @@ impl Move { } match self { - Move::Promotion { data, piece } => todo!(), - Move::Castle { data } => todo!(), - Move::Normal { data } => { - let pc = node.pos.get_piece(data.src).unwrap(); + Move::Promotion(data, piece) => todo!(), + Move::Castle(data) => todo!(), + Move::Normal(data) => { + let pc_src = node.pos.get_piece(data.src).unwrap(); + if matches!(pc_src.pc, crate::Piece::Pawn) { + // pawn moves are irreversible + node.pos.half_moves = 0; + + // en-passant + if data.src.0 + (BOARD_WIDTH) * 2 == data.dest.0 { + node.pos.ep_square = Some( + Square::try_from(data.src.0 + BOARD_WIDTH) + .expect("En-passant target should be valid."), + ) + } else if data.dest.0 + (BOARD_WIDTH) * 2 == data.src.0 { + node.pos.ep_square = Some( + Square::try_from(data.src.0 - BOARD_WIDTH) + .expect("En-passant target should be valid."), + ) + } else { + node.pos.ep_square = None; + } + } else { + node.pos.half_moves += 1; + node.pos.ep_square = None; + } + + if let Some(_pc_dest) = node.pos.get_piece(data.dest) { + // captures are irreversible + node.pos.half_moves = 0; + } + node.pos.del_piece(data.src); - node.pos.set_piece(data.dest, pc); + node.pos.set_piece(data.dest, pc_src); } - Move::EnPassant { data } => todo!(), + Move::EnPassant(data) => todo!(), } node } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::fen::START_POSITION; + + /// Test that make move works for regular piece pushes and captures. + /// + /// Also tests en passant target square. + #[test] + fn test_normal_move() { + let start_pos = START_POSITION; + // (src, dest, expected fen) + // FENs made with https://lichess.org/analysis + // En-passant target square is manually added, since Lichess doesn't have it when + // en-passant is not legal. + let moves = [ + ( + "e2", + "e4", + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", + ), + ( + "e7", + "e5", + "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2", + ), + ( + "g1", + "f3", + "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2", + ), + ( + "g8", + "f6", + "rnbqkb1r/pppp1ppp/5n2/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", + ), + ( + "f1", + "c4", + "rnbqkb1r/pppp1ppp/5n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3", + ), + ( + "f8", + "c5", + "rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4", + ), + ( + "d1", + "e2", + "rnbqk2r/pppp1ppp/5n2/2b1p3/2B1P3/5N2/PPPPQPPP/RNB1K2R b KQkq - 5 4", + ), + ( + "d8", + "e7", + "rnb1k2r/ppppqppp/5n2/2b1p3/2B1P3/5N2/PPPPQPPP/RNB1K2R w KQkq - 6 5", + ), + ( + "f3", + "e5", + "rnb1k2r/ppppqppp/5n2/2b1N3/2B1P3/8/PPPPQPPP/RNB1K2R b KQkq - 0 5", + ), + ( + "e7", + "e5", + "rnb1k2r/pppp1ppp/5n2/2b1q3/2B1P3/8/PPPPQPPP/RNB1K2R w KQkq - 0 6", + ), + ]; + + let mut node = Node::default(); + + for (src, dest, expect_fen) in moves { + let idx_src = Square::from_algebraic(src.to_string()).unwrap(); + let idx_dest = Square::from_algebraic(dest.to_string()).unwrap(); + let mv = Move::Normal(MoveData { + src: idx_src, + dest: idx_dest, + }); + node = mv.make(node); + assert_eq!(node.pos.to_fen(), expect_fen.to_string()) + } + } +}