stub: movegen

This commit is contained in:
dogeystamp 2024-09-29 10:43:45 -04:00
parent 1914d812e4
commit 77838fd417
5 changed files with 124 additions and 52 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
TODO.txt

View File

@ -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);
}

View File

@ -1,4 +1,4 @@
use crate::{Position, Index, Color, ColPiece}; use crate::{BoardState, Square, Color, ColPiece};
use crate::{BOARD_WIDTH, BOARD_HEIGHT}; use crate::{BOARD_WIDTH, BOARD_HEIGHT};
pub trait FromFen { pub trait FromFen {
@ -29,9 +29,9 @@ pub enum FenError {
InternalError(usize), InternalError(usize),
} }
impl FromFen for Position { impl FromFen for BoardState {
type Error = FenError; type Error = FenError;
fn from_fen(fen: String) -> Result<Position, FenError> { fn from_fen(fen: String) -> Result<BoardState, FenError> {
//! Parse FEN string into position. //! Parse FEN string into position.
/// Parser state machine. /// Parser state machine.
@ -55,7 +55,7 @@ impl FromFen for Position {
FullMove, FullMove,
} }
let mut pos = Position::default(); let mut pos = BoardState::default();
let mut parser_state = FenState::Piece(0, 0); let mut parser_state = FenState::Piece(0, 0);
let mut next_state = FenState::Space; 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))?; let pc = ColPiece::try_from(pc_char).or(bad_char!(i))?;
pos.set_piece( pos.set_piece(
Index::from_row_col(real_row, col) Square::from_row_col(real_row, col)
.or(Err(FenError::InternalError(i)))?, .or(Err(FenError::InternalError(i)))?,
pc, pc,
); );
@ -174,7 +174,7 @@ impl FromFen for Position {
parse_space_and_goto!(FenState::HalfMove); parse_space_and_goto!(FenState::HalfMove);
} }
'a'..='h' => { '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; parser_state = FenState::EnPassantFile;
} }
_ => return bad_char!(i), _ => return bad_char!(i),
@ -182,8 +182,8 @@ impl FromFen for Position {
} }
FenState::EnPassantFile => { FenState::EnPassantFile => {
if let Some(digit) = c.to_digit(10) { if let Some(digit) = c.to_digit(10) {
pos.ep_square = Some(Index( pos.ep_square = Some(Square(
usize::from(pos.ep_square.unwrap_or(Index(0))) usize::from(pos.ep_square.unwrap_or(Square(0)))
+ (digit as usize - 1) * 8, + (digit as usize - 1) * 8,
)); ));
} else { } else {
@ -193,7 +193,7 @@ impl FromFen for Position {
} }
FenState::HalfMove => { FenState::HalfMove => {
if let Some(digit) = c.to_digit(10) { 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); return Err(FenError::TooManyMoves);
} }
pos.half_moves *= 10; pos.half_moves *= 10;
@ -206,7 +206,7 @@ impl FromFen for Position {
} }
FenState::FullMove => { FenState::FullMove => {
if let Some(digit) = c.to_digit(10) { 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); return Err(FenError::TooManyMoves);
} }
pos.full_moves *= 10; pos.full_moves *= 10;
@ -237,22 +237,22 @@ mod tests {
#[test] #[test]
fn test_fen_pieces() { fn test_fen_pieces() {
let fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"; 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!( assert_eq!(
(0..N_SQUARES) (0..N_SQUARES)
.map(Index) .map(Square)
.map(|i| board.get_piece(i)) .map(|i| board.get_piece(i))
.map(ColPiece::opt_to_char) .map(ColPiece::opt_to_char)
.collect::<String>(), .collect::<String>(),
"RNBQKBNRPPPP.PPP............P...................pppppppprnbqkbnr" "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); assert_eq!(board.turn, Color::Black);
} }
macro_rules! make_board{ macro_rules! make_board{
($fen_fmt: expr) => { ($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)]; let test_cases = [("e3", 20), ("h8", 63), ("a8", 56), ("h4", 31), ("a1", 0)];
for (sqr, idx) in test_cases { for (sqr, idx) in test_cases {
let board = make_board!("8/8/8/8/8/8/8/8 w - {sqr} 0 0"); 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"); let board = make_board!("8/8/8/8/8/8/8/8 w - - 0 0");
@ -338,7 +338,7 @@ mod tests {
#[test] #[test]
fn test_fen_half_move_counter() { 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"); let board = make_board!("8/8/8/8/8/8/8/8 w - - {i} 0");
assert_eq!(board.half_moves, i); assert_eq!(board.half_moves, i);
assert_eq!(board.full_moves, 0); assert_eq!(board.full_moves, 0);
@ -347,7 +347,7 @@ mod tests {
#[test] #[test]
fn test_fen_move_counter() { 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}"); let board = make_board!("8/8/8/8/8/8/8/8 w - - 0 {i}");
assert_eq!(board.half_moves, 0); assert_eq!(board.half_moves, 0);
assert_eq!(board.full_moves, i); assert_eq!(board.full_moves, i);

View File

@ -1,6 +1,8 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
pub mod fen; pub mod fen;
pub mod movegen;
use std::rc::Rc;
const BOARD_WIDTH: usize = 8; const BOARD_WIDTH: usize = 8;
const BOARD_HEIGHT: 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)] #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
enum Color { enum Color {
#[default] #[default]
White, White = 0,
Black, Black = 1,
} }
const N_COLORS: usize = 2; 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)] #[derive(Debug, Copy, Clone)]
enum Piece { enum Piece {
Rook, Rook,
@ -88,29 +100,29 @@ impl ColPiece {
/// ///
/// A1 is (0, 0) -> 0, A2 is (0, 1) -> 2, and H8 is (7, 7) -> 63. /// 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)]
struct Index(usize); struct Square(usize);
enum IndexError { enum IndexError {
OutOfBounds, OutOfBounds,
} }
impl TryFrom<usize> for Index { impl TryFrom<usize> for Square {
type Error = IndexError; type Error = IndexError;
fn try_from(value: usize) -> Result<Self, Self::Error> { fn try_from(value: usize) -> Result<Self, Self::Error> {
if (0..N_SQUARES).contains(&value) { if (0..N_SQUARES).contains(&value) {
Ok(Index(value)) Ok(Square(value))
} else { } else {
Err(IndexError::OutOfBounds) Err(IndexError::OutOfBounds)
} }
} }
} }
impl From<Index> for usize { impl From<Square> for usize {
fn from(value: Index) -> Self { fn from(value: Square) -> Self {
value.0 value.0
} }
} }
impl Index { impl Square {
fn from_row_col(r: usize, c: usize) -> Result<Self, IndexError> { fn from_row_col(r: usize, c: usize) -> Result<Self, IndexError> {
//! Get index of square based on row and column. //! Get index of square based on row and column.
let ret = BOARD_WIDTH * r + c; let ret = BOARD_WIDTH * r + c;
@ -147,16 +159,16 @@ impl From<Piece> for char {
} }
} }
#[derive(Default, Debug)] #[derive(Default, Debug, Clone, Copy)]
struct Bitboard(u64); struct Bitboard(u64);
impl Bitboard { 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. //! Set the square at an index to on.
self.0 |= 1 << usize::from(idx); 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. //! Set the square at an index to off.
self.0 &= !(1 << usize::from(idx)); self.0 &= !(1 << usize::from(idx));
} }
@ -165,7 +177,7 @@ impl Bitboard {
/// Array form board. /// Array form board.
/// ///
/// Complements bitboards, notably for "what piece is at this square?" queries. /// Complements bitboards, notably for "what piece is at this square?" queries.
#[derive(Debug)] #[derive(Debug, Clone, Copy)]
struct Mailbox([Option<ColPiece>; N_SQUARES]); struct Mailbox([Option<ColPiece>; N_SQUARES]);
impl Default for Mailbox { impl Default for Mailbox {
@ -176,12 +188,12 @@ impl Default for Mailbox {
impl Mailbox { impl Mailbox {
/// Get mutable reference to square at index. /// Get mutable reference to square at index.
fn sq_mut(&mut self, idx: Index) -> &mut Option<ColPiece> { fn sq_mut(&mut self, idx: Square) -> &mut Option<ColPiece> {
&mut self.0[usize::from(idx)] &mut self.0[usize::from(idx)]
} }
/// Get non-mutable reference to square at index. /// Get non-mutable reference to square at index.
fn sq(&self, idx: Index) -> &Option<ColPiece> { fn sq(&self, idx: Square) -> &Option<ColPiece> {
&self.0[usize::from(idx)] &self.0[usize::from(idx)]
} }
} }
@ -189,7 +201,7 @@ impl Mailbox {
/// Piece bitboards and state for one player. /// Piece bitboards and state for one player.
/// ///
/// Default is all empty. /// Default is all empty.
#[derive(Default, Debug)] #[derive(Default, Debug, Clone, Copy)]
struct Player { struct Player {
/// Bitboards for individual pieces. Piece -> locations. /// Bitboards for individual pieces. Piece -> locations.
bit: [Bitboard; N_PIECES], bit: [Bitboard; N_PIECES],
@ -203,7 +215,7 @@ impl Player {
} }
/// Castling rights for one player /// Castling rights for one player
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub struct CastlingRights { pub struct CastlingRights {
/// Kingside /// Kingside
k: bool, k: bool,
@ -211,11 +223,11 @@ pub struct CastlingRights {
q: bool, q: bool,
} }
/// Game state. /// Immutable game state, unique to a position.
/// ///
/// Default is empty. /// Default is empty.
#[derive(Debug, Default)] #[derive(Debug, Default, Clone, Copy)]
pub struct Position { pub struct BoardState {
/// Player bitboards /// Player bitboards
players: [Player; N_COLORS], players: [Player; N_COLORS],
@ -225,7 +237,7 @@ pub struct Position {
/// En-passant square. /// En-passant square.
/// ///
/// (If a pawn moves twice, this is one square in front of the start position.) /// (If a pawn moves twice, this is one square in front of the start position.)
ep_square: Option<Index>, ep_square: Option<Square>,
/// Castling rights /// Castling rights
castle: [CastlingRights; N_COLORS], castle: [CastlingRights; N_COLORS],
@ -240,21 +252,21 @@ pub struct Position {
turn: Color, turn: Color,
} }
impl Position { impl BoardState {
/// Get mutable reference to a player. /// Get mutable reference to a player.
fn pl_mut(&mut self, col: Color) -> &mut Player { fn pl_mut(&mut self, col: Color) -> &mut Player {
&mut self.players[col as usize] &mut self.players[col as usize]
} }
/// Create a new piece in a location. /// 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); let pl = self.pl_mut(pc.col);
pl.board(pc.into()).on_idx(idx); pl.board(pc.into()).on_idx(idx);
*self.mail.sq_mut(idx) = Some(pc); *self.mail.sq_mut(idx) = Some(pc);
} }
/// Delete the piece in a location, if it exists. /// 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) { if let Some(pc) = *self.mail.sq_mut(idx) {
let pl = self.pl_mut(pc.col); let pl = self.pl_mut(pc.col);
pl.board(pc.into()).off_idx(idx); pl.board(pc.into()).off_idx(idx);
@ -263,7 +275,7 @@ impl Position {
} }
/// Get the piece at a location. /// Get the piece at a location.
fn get_piece(&self, idx: Index) -> Option<ColPiece> { fn get_piece(&self, idx: Square) -> Option<ColPiece> {
*self.mail.sq(idx) *self.mail.sq(idx)
} }
@ -271,12 +283,12 @@ impl Position {
const MAX_MOVES: usize = 9_999; 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut str = String::with_capacity(N_SQUARES + BOARD_HEIGHT); let mut str = String::with_capacity(N_SQUARES + BOARD_HEIGHT);
for row in (0..BOARD_HEIGHT).rev() { for row in (0..BOARD_HEIGHT).rev() {
for col in 0..BOARD_WIDTH { 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); let pc = self.get_piece(idx);
str.push(ColPiece::opt_to_char(pc)); str.push(ColPiece::opt_to_char(pc));
} }

68
src/movegen.rs Normal file
View File

@ -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<Rc<Node>>,
}
/// 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
}
}