From 77838fd417406c2ed31a82875f848b69225c66f9 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Sun, 29 Sep 2024 10:43:45 -0400 Subject: [PATCH] stub: movegen --- .gitignore | 1 + src/bin/scratch.rs | 9 ------ src/fen.rs | 34 +++++++++++------------ src/lib.rs | 64 +++++++++++++++++++++++++------------------ src/movegen.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 52 deletions(-) delete mode 100644 src/bin/scratch.rs create mode 100644 src/movegen.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..a2ee6b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +TODO.txt diff --git a/src/bin/scratch.rs b/src/bin/scratch.rs deleted file mode 100644 index 8255c28..0000000 --- a/src/bin/scratch.rs +++ /dev/null @@ -1,9 +0,0 @@ -use chess_inator::Position; -use chess_inator::fen::FromFen; - -fn main() { - let fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"; - let board = Position::from_fen(fen.into()).unwrap(); - println!("{}", board); - println!("{:#?}", board); -} diff --git a/src/fen.rs b/src/fen.rs index cab8655..436aff8 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::{Position, Index, Color, ColPiece}; +use crate::{BoardState, Square, Color, ColPiece}; use crate::{BOARD_WIDTH, BOARD_HEIGHT}; pub trait FromFen { @@ -29,9 +29,9 @@ pub enum FenError { InternalError(usize), } -impl FromFen for Position { +impl FromFen for BoardState { type Error = FenError; - fn from_fen(fen: String) -> Result { + fn from_fen(fen: String) -> Result { //! Parse FEN string into position. /// Parser state machine. @@ -55,7 +55,7 @@ impl FromFen for Position { FullMove, } - let mut pos = Position::default(); + let mut pos = BoardState::default(); let mut parser_state = FenState::Piece(0, 0); let mut next_state = FenState::Space; @@ -105,7 +105,7 @@ impl FromFen for Position { let pc = ColPiece::try_from(pc_char).or(bad_char!(i))?; pos.set_piece( - Index::from_row_col(real_row, col) + Square::from_row_col(real_row, col) .or(Err(FenError::InternalError(i)))?, pc, ); @@ -174,7 +174,7 @@ impl FromFen for Position { parse_space_and_goto!(FenState::HalfMove); } 'a'..='h' => { - pos.ep_square = Some(Index(c as usize - 'a' as usize)); + pos.ep_square = Some(Square(c as usize - 'a' as usize)); parser_state = FenState::EnPassantFile; } _ => return bad_char!(i), @@ -182,8 +182,8 @@ impl FromFen for Position { } FenState::EnPassantFile => { if let Some(digit) = c.to_digit(10) { - pos.ep_square = Some(Index( - usize::from(pos.ep_square.unwrap_or(Index(0))) + pos.ep_square = Some(Square( + usize::from(pos.ep_square.unwrap_or(Square(0))) + (digit as usize - 1) * 8, )); } else { @@ -193,7 +193,7 @@ impl FromFen for Position { } FenState::HalfMove => { if let Some(digit) = c.to_digit(10) { - if pos.half_moves > Position::MAX_MOVES { + if pos.half_moves > BoardState::MAX_MOVES { return Err(FenError::TooManyMoves); } pos.half_moves *= 10; @@ -206,7 +206,7 @@ impl FromFen for Position { } FenState::FullMove => { if let Some(digit) = c.to_digit(10) { - if pos.half_moves > Position::MAX_MOVES { + if pos.half_moves > BoardState::MAX_MOVES { return Err(FenError::TooManyMoves); } pos.full_moves *= 10; @@ -237,22 +237,22 @@ mod tests { #[test] fn test_fen_pieces() { let fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"; - let board = Position::from_fen(fen.into()).unwrap(); + let board = BoardState::from_fen(fen.into()).unwrap(); assert_eq!( (0..N_SQUARES) - .map(Index) + .map(Square) .map(|i| board.get_piece(i)) .map(ColPiece::opt_to_char) .collect::(), "RNBQKBNRPPPP.PPP............P...................pppppppprnbqkbnr" ); - assert_eq!(board.ep_square.unwrap(), Index(20)); + assert_eq!(board.ep_square.unwrap(), Square(20)); assert_eq!(board.turn, Color::Black); } macro_rules! make_board{ ($fen_fmt: expr) => { - Position::from_fen(format!($fen_fmt)).unwrap() + BoardState::from_fen(format!($fen_fmt)).unwrap() } } @@ -261,7 +261,7 @@ mod tests { let test_cases = [("e3", 20), ("h8", 63), ("a8", 56), ("h4", 31), ("a1", 0)]; for (sqr, idx) in test_cases { let board = make_board!("8/8/8/8/8/8/8/8 w - {sqr} 0 0"); - assert_eq!(board.ep_square.unwrap(), Index(idx)); + assert_eq!(board.ep_square.unwrap(), Square(idx)); } let board = make_board!("8/8/8/8/8/8/8/8 w - - 0 0"); @@ -338,7 +338,7 @@ mod tests { #[test] fn test_fen_half_move_counter() { - for i in 0..=Position::MAX_MOVES { + for i in 0..=BoardState::MAX_MOVES { let board = make_board!("8/8/8/8/8/8/8/8 w - - {i} 0"); assert_eq!(board.half_moves, i); assert_eq!(board.full_moves, 0); @@ -347,7 +347,7 @@ mod tests { #[test] fn test_fen_move_counter() { - for i in 0..=Position::MAX_MOVES { + for i in 0..=BoardState::MAX_MOVES { let board = make_board!("8/8/8/8/8/8/8/8 w - - 0 {i}"); assert_eq!(board.half_moves, 0); assert_eq!(board.full_moves, i); diff --git a/src/lib.rs b/src/lib.rs index 3389a8f..2fc50aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ #![deny(rust_2018_idioms)] pub mod fen; +pub mod movegen; +use std::rc::Rc; const BOARD_WIDTH: usize = 8; const BOARD_HEIGHT: usize = 8; @@ -9,11 +11,21 @@ const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT; #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] enum Color { #[default] - White, - Black, + White = 0, + Black = 1, } const N_COLORS: usize = 2; +impl Color { + /// Return opposite color (does not assign). + pub fn flip(self) -> Self { + match self { + Color::White => Color::Black, + Color::Black => Color::White, + } + } +} + #[derive(Debug, Copy, Clone)] enum Piece { Rook, @@ -88,29 +100,29 @@ impl ColPiece { /// /// A1 is (0, 0) -> 0, A2 is (0, 1) -> 2, and H8 is (7, 7) -> 63. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct Index(usize); +struct Square(usize); enum IndexError { OutOfBounds, } -impl TryFrom for Index { +impl TryFrom for Square { type Error = IndexError; fn try_from(value: usize) -> Result { if (0..N_SQUARES).contains(&value) { - Ok(Index(value)) + Ok(Square(value)) } else { Err(IndexError::OutOfBounds) } } } -impl From for usize { - fn from(value: Index) -> Self { +impl From for usize { + fn from(value: Square) -> Self { value.0 } } -impl Index { +impl Square { fn from_row_col(r: usize, c: usize) -> Result { //! Get index of square based on row and column. let ret = BOARD_WIDTH * r + c; @@ -147,16 +159,16 @@ impl From for char { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, Copy)] struct Bitboard(u64); impl Bitboard { - pub fn on_idx(&mut self, idx: Index) { + pub fn on_idx(&mut self, idx: Square) { //! Set the square at an index to on. self.0 |= 1 << usize::from(idx); } - pub fn off_idx(&mut self, idx: Index) { + pub fn off_idx(&mut self, idx: Square) { //! Set the square at an index to off. self.0 &= !(1 << usize::from(idx)); } @@ -165,7 +177,7 @@ impl Bitboard { /// Array form board. /// /// Complements bitboards, notably for "what piece is at this square?" queries. -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] struct Mailbox([Option; N_SQUARES]); impl Default for Mailbox { @@ -176,12 +188,12 @@ impl Default for Mailbox { impl Mailbox { /// Get mutable reference to square at index. - fn sq_mut(&mut self, idx: Index) -> &mut Option { + fn sq_mut(&mut self, idx: Square) -> &mut Option { &mut self.0[usize::from(idx)] } /// Get non-mutable reference to square at index. - fn sq(&self, idx: Index) -> &Option { + fn sq(&self, idx: Square) -> &Option { &self.0[usize::from(idx)] } } @@ -189,7 +201,7 @@ impl Mailbox { /// Piece bitboards and state for one player. /// /// Default is all empty. -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, Copy)] struct Player { /// Bitboards for individual pieces. Piece -> locations. bit: [Bitboard; N_PIECES], @@ -203,7 +215,7 @@ impl Player { } /// Castling rights for one player -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] pub struct CastlingRights { /// Kingside k: bool, @@ -211,11 +223,11 @@ pub struct CastlingRights { q: bool, } -/// Game state. +/// Immutable game state, unique to a position. /// /// Default is empty. -#[derive(Debug, Default)] -pub struct Position { +#[derive(Debug, Default, Clone, Copy)] +pub struct BoardState { /// Player bitboards players: [Player; N_COLORS], @@ -225,7 +237,7 @@ pub struct Position { /// En-passant square. /// /// (If a pawn moves twice, this is one square in front of the start position.) - ep_square: Option, + ep_square: Option, /// Castling rights castle: [CastlingRights; N_COLORS], @@ -240,21 +252,21 @@ pub struct Position { turn: Color, } -impl Position { +impl BoardState { /// Get mutable reference to a player. fn pl_mut(&mut self, col: Color) -> &mut Player { &mut self.players[col as usize] } /// Create a new piece in a location. - fn set_piece(&mut self, idx: Index, pc: ColPiece) { + fn set_piece(&mut self, idx: Square, pc: ColPiece) { let pl = self.pl_mut(pc.col); pl.board(pc.into()).on_idx(idx); *self.mail.sq_mut(idx) = Some(pc); } /// Delete the piece in a location, if it exists. - fn del_piece(&mut self, idx: Index) { + fn del_piece(&mut self, idx: Square) { if let Some(pc) = *self.mail.sq_mut(idx) { let pl = self.pl_mut(pc.col); pl.board(pc.into()).off_idx(idx); @@ -263,7 +275,7 @@ impl Position { } /// Get the piece at a location. - fn get_piece(&self, idx: Index) -> Option { + fn get_piece(&self, idx: Square) -> Option { *self.mail.sq(idx) } @@ -271,12 +283,12 @@ impl Position { const MAX_MOVES: usize = 9_999; } -impl core::fmt::Display for Position { +impl core::fmt::Display for BoardState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut str = String::with_capacity(N_SQUARES + BOARD_HEIGHT); for row in (0..BOARD_HEIGHT).rev() { for col in 0..BOARD_WIDTH { - let idx = Index::from_row_col(row, col).or(Err(std::fmt::Error))?; + let idx = Square::from_row_col(row, col).or(Err(std::fmt::Error))?; let pc = self.get_piece(idx); str.push(ColPiece::opt_to_char(pc)); } diff --git a/src/movegen.rs b/src/movegen.rs new file mode 100644 index 0000000..508c2f4 --- /dev/null +++ b/src/movegen.rs @@ -0,0 +1,68 @@ +//! Move generation. + +use crate::{Color, Square, BoardState}; +use std::rc::Rc; + +/// Game tree node. +struct Node { + /// Immutable position data. + pos: BoardState, + /// Backlink to previous node. + prev: Option>, +} + +/// Piece enum specifically for promotions. +#[derive(Debug, Copy, Clone)] +enum PromotePiece { + Rook, + Bishop, + Knight, + Queen, +} + +/// Move data common to all move types. +struct MoveData { + src: Square, + dest: Square, +} +/// Pseudo-legal move. +enum Move { + /// Pawn promotes to another piece. + Promotion { data: MoveData, piece: PromotePiece }, + /// King castles with rook. + Castle { data: MoveData }, + /// Capture, or push move. + Normal { data: MoveData }, + /// This move is an en-passant capture. + EnPassant { data: MoveData }, +} + +impl Move { + /// Make move and return new position. + /// + /// Old position is saved in a backlink. + pub fn make(self, old_node: Node) -> Node { + let old_pos = old_node.pos; + let mut node = Node { + prev: Some(Rc::new(old_node)), + pos: old_pos, + }; + node.pos.turn = node.pos.turn.flip(); + if node.pos.turn == Color::White { + node.pos.full_moves += 1; + } + + match self { + Move::Promotion { data, piece } => todo!(), + Move::Castle { data } => todo!(), + Move::Normal { data } => { + let pc = node.pos.get_piece(data.src).unwrap(); + node.pos.del_piece(data.src); + node.pos.set_piece(data.dest, pc); + } + Move::EnPassant { data } => todo!(), + } + + node + } +}