diff --git a/src/fen.rs b/src/fen.rs index 78af921..4ba95b8 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -4,7 +4,7 @@ pub const START_POSITION: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w pub trait FromFen { type Error; - fn from_fen(_: String) -> Result + fn from_fen(_: &str) -> Result where Self: std::marker::Sized; } @@ -36,7 +36,7 @@ pub enum FenError { impl FromFen for BoardState { type Error = FenError; - fn from_fen(fen: String) -> Result { + fn from_fen(fen: &str) -> Result { //! Parse FEN string into position. /// Parser state machine. @@ -301,7 +301,7 @@ mod tests { macro_rules! make_board { ($fen_fmt: expr) => { - BoardState::from_fen(format!($fen_fmt)).unwrap() + BoardState::from_fen(&format!($fen_fmt)).unwrap() }; } @@ -437,7 +437,7 @@ mod tests { for fen1 in test_cases { println!("fen1: {fen1:?}"); - let fen2 = BoardState::from_fen(fen1.to_string()).unwrap().to_fen(); + let fen2 = BoardState::from_fen(fen1).unwrap().to_fen(); assert_eq!(fen1.to_string(), fen2, "FEN not equivalent") } diff --git a/src/lib.rs b/src/lib.rs index 3bdf730..328576a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ #![deny(rust_2018_idioms)] +use std::str::FromStr; + pub mod fen; pub mod movegen; @@ -107,7 +109,7 @@ impl ColPiece { /// Square index newtype. /// /// A1 is (0, 0) -> 0, A2 is (0, 1) -> 2, and H8 is (7, 7) -> 63. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct Square(usize); #[derive(Debug)] @@ -127,6 +129,28 @@ impl TryFrom for Square { } } } + +macro_rules! sq_try_from { + ($T: ty) => { + impl TryFrom<$T> for Square { + type Error = SquareError; + + fn try_from(value: $T) -> Result { + if let Ok(upper_bound) = <$T>::try_from(N_SQUARES) { + if (0..upper_bound).contains(&value) { + return Ok(Square(value as usize)); + } + } + Err(SquareError::OutOfBounds) + } + } + }; +} + +sq_try_from!(i32); +sq_try_from!(isize); +sq_try_from!(i8); + impl From for usize { fn from(value: Square) -> Self { value.0 @@ -138,6 +162,10 @@ impl Square { let ret = BOARD_WIDTH * r + c; ret.try_into() } + fn from_row_col_signed(r: isize, c: isize) -> Result { + let ret = (BOARD_WIDTH as isize) * r + c; + ret.try_into() + } fn to_row_col(self) -> (usize, usize) { //! Get row, column from index let div = self.0 / BOARD_WIDTH; @@ -155,10 +183,14 @@ impl Square { let file = letters[col]; format!("{file}{rank}") } +} + +impl FromStr for Square { + type Err = SquareError; /// Convert typical human-readable form (e.g. `e4`) to square index. - fn from_algebraic(value: &str) -> Result { - let bytes = value.as_bytes(); + fn from_str(s: &str) -> Result { + let bytes = s.as_bytes(); let col = match bytes[0] as char { 'a' => 0, 'b' => 1, @@ -289,8 +321,13 @@ struct Player { } impl Player { - /// Get board for a specific piece. - fn board(&mut self, pc: Piece) -> &mut Bitboard { + /// Get board (non-mutable) for a specific piece. + fn board(&self, pc: Piece) -> &Bitboard { + &self.bit[pc as usize] + } + + /// Get board (mutable) for a specific piece. + fn board_mut(&mut self, pc: Piece) -> &mut Bitboard { &mut self.bit[pc as usize] } } @@ -371,7 +408,7 @@ impl BoardState { /// Create a new piece in a location. fn set_piece(&mut self, idx: Square, pc: ColPiece) { let pl = self.pl_mut(pc.col); - pl.board(pc.into()).on_idx(idx); + pl.board_mut(pc.into()).on_idx(idx); *self.mail.sq_mut(idx) = Some(pc); } @@ -381,7 +418,7 @@ impl BoardState { fn del_piece(&mut self, idx: Square) -> Result { if let Some(pc) = *self.mail.sq_mut(idx) { let pl = self.pl_mut(pc.col); - pl.board(pc.into()).off_idx(idx); + pl.board_mut(pc.into()).off_idx(idx); *self.mail.sq_mut(idx) = None; Ok(pc) } else { @@ -422,13 +459,46 @@ impl core::fmt::Display for BoardState { mod tests { use super::*; + #[test] + fn test_square_casts() { + let fail_cases = [-1, 64, 0x7FFFFFFF, 257, 256, 128, 65, -3, !0x7FFFFFFF]; + for tc in fail_cases { + macro_rules! try_type { + ($T: ty) => { + if let Ok(conv) = <$T>::try_from(tc) { + assert!(matches!(Square::try_from(conv), Err(SquareError::OutOfBounds))) + } + }; + } + try_type!(i32); + try_type!(i8); + try_type!(isize); + try_type!(usize); + } + + let good_cases = 0..N_SQUARES; + for tc in good_cases { + macro_rules! try_type { + ($T: ty) => { + let conv = <$T>::try_from(tc).unwrap(); + let res = Square::try_from(conv).unwrap(); + assert_eq!(res.0, tc); + }; + } + try_type!(i32); + try_type!(i8); + try_type!(isize); + try_type!(usize); + } + } + #[test] 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).unwrap(), + sqr.parse::().unwrap(), Square::try_from(idx).unwrap() ); } diff --git a/src/movegen.rs b/src/movegen.rs index 55834ed..1b266e5 100644 --- a/src/movegen.rs +++ b/src/movegen.rs @@ -18,15 +18,14 @@ struct Node { impl Default for Node { fn default() -> Self { Node { - pos: BoardState::from_fen(START_POSITION.to_string()) - .expect("Starting FEN should be valid"), + pos: BoardState::from_fen(START_POSITION).expect("Starting FEN should be valid"), prev: None, } } } /// Piece enum specifically for promotions. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] enum PromotePiece { Rook, Bishop, @@ -45,17 +44,18 @@ impl From for Piece { } } -/// Pseudo-legal move. -/// -/// No checking is done when constructing this. +#[derive(PartialEq, Eq, PartialOrd, Ord)] enum MoveType { /// Pawn promotes to another piece. Promotion(PromotePiece), /// Capture, or push move. Includes castling and en-passant too. Normal, } -/// Move data common to all move types. -struct Move { +/// Pseudo-legal move. +/// +/// No checking is done when constructing this. +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub struct Move { src: Square, dest: Square, move_type: MoveType, @@ -232,7 +232,7 @@ pub trait ToUCIAlgebraic { } #[derive(Debug)] -enum MoveAlgebraicError { +pub enum MoveAlgebraicError { /// String is invalid length; refuse to parse InvalidLength(usize), /// Invalid character at given index. @@ -250,14 +250,14 @@ impl FromUCIAlgebraic for Move { return Err(MoveAlgebraicError::InvalidLength(value_len)); } - let src_sq = match Square::from_algebraic(&value[0..=1]) { + let src_sq = match value[0..=1].parse::() { Ok(sq) => sq, Err(e) => { return Err(MoveAlgebraicError::SquareError(0, e)); } }; - let dest_sq = match Square::from_algebraic(&value[2..=3]) { + let dest_sq = match value[2..=3].parse::() { Ok(sq) => sq, Err(e) => { return Err(MoveAlgebraicError::SquareError(0, e)); @@ -285,11 +285,144 @@ impl FromUCIAlgebraic for Move { } } +/// Pseudo-legal move generation. +/// +/// "Pseudo-legal" here means that moving into check is allowed, and capturing friendly pieces is +/// allowed. These will be filtered out in the legal move generation step. +pub trait PseudoMoveGen { + type MoveIterable; + fn gen_pseudo_moves(self) -> Self::MoveIterable; +} + +enum SliderDirection { + /// Rook movement + Straight, + /// Bishop movement + Diagonal, + /// Queen/king movement + Star, +} +/// Generate slider moves for a given square. +/// +/// # Arguments +/// +/// * `board`: Board to generate moves with. +/// * `src`: Square on which the slider piece is on. +/// * `move_list`: Vector to append generated moves to. +/// * `slide_type`: Directions the piece is allowed to go in. +/// * `keep_going`: Allow sliding more than one square (true for everything except king). +fn move_slider( + board: &BoardState, + src: Square, + move_list: &mut Vec, + slide_type: SliderDirection, + keep_going: bool, +) { + let dirs_straight = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + let dirs_diag = [(1, 1), (1, -1), (-1, 1), (-1, -1)]; + let dirs_star = [ + (1, 1), + (1, -1), + (-1, 1), + (-1, -1), + (0, 1), + (1, 0), + (-1, 0), + (0, -1), + ]; + + let dirs = match slide_type { + SliderDirection::Straight => dirs_straight.iter(), + SliderDirection::Diagonal => dirs_diag.iter(), + SliderDirection::Star => dirs_star.iter(), + }; + + for dir in dirs { + let (mut r, mut c) = src.to_row_col(); + loop { + // increment + let nr = r as isize + dir.0; + let nc = c as isize + dir.1; + + if let Ok(dest) = Square::from_row_col_signed(nr, nc) { + r = nr as usize; + c = nc as usize; + + move_list.push(Move { + src, + dest, + move_type: MoveType::Normal, + }); + + // Stop at other pieces. + if let Some(_cap_pc) = board.get_piece(dest) { + break; + } + } else { + break; + } + + if !keep_going { + break; + } + } + } +} + +impl PseudoMoveGen for BoardState { + type MoveIterable = Vec; + + fn gen_pseudo_moves(self) -> Self::MoveIterable { + let mut ret = Vec::new(); + for pl in self.players { + for sq in pl.board(Piece::Rook).into_iter() { + move_slider(&self, sq, &mut ret, SliderDirection::Straight, true); + } + } + ret + } +} + +/// Legal move generation. +pub trait LegalMoveGen { + type MoveIterable; + fn gen_moves(self) -> Self::MoveIterable; +} + #[cfg(test)] mod tests { use super::*; use crate::fen::{ToFen, START_POSITION}; + /// Test that slider pieces can move and capture. + #[test] + fn test_slider_movegen() { + let test_cases = [( + // start position + "8/8/8/8/8/8/8/R7 w - - 0 1", + // expected moves + vec![( + // source piece + "a1", + // destination squares + vec![ + "a2", "a3", "a4", "a5", "a6", "a7", "a8", "b1", "c1", "d1", "e1", "f1", "g1", + "h1", + ], + )], + )]; + + for (fen, expected) in test_cases { + let board = BoardState::from_fen(fen).unwrap(); + + let mut moves = board.gen_pseudo_moves(); + moves.sort_unstable(); + let moves = moves; + + let expected_moves = expected.iter().map(|(src, dests)| {}); + } + } + /// Test that make move and unmake move work as expected. /// /// Ensure that: @@ -405,7 +538,7 @@ mod tests { // make move eprintln!("Starting test case {i}, make move."); let mut node = Node { - pos: BoardState::from_fen(start_pos.to_string()).unwrap(), + pos: BoardState::from_fen(start_pos).unwrap(), prev: None, }; for (move_str, expect_fen) in moves {