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
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};
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<Position, FenError> {
fn from_fen(fen: String) -> Result<BoardState, FenError> {
//! 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::<String>(),
"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);

View File

@ -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<usize> for Index {
impl TryFrom<usize> for Square {
type Error = IndexError;
fn try_from(value: usize) -> Result<Self, Self::Error> {
if (0..N_SQUARES).contains(&value) {
Ok(Index(value))
Ok(Square(value))
} else {
Err(IndexError::OutOfBounds)
}
}
}
impl From<Index> for usize {
fn from(value: Index) -> Self {
impl From<Square> for usize {
fn from(value: Square) -> Self {
value.0
}
}
impl Index {
impl Square {
fn from_row_col(r: usize, c: usize) -> Result<Self, IndexError> {
//! Get index of square based on row and column.
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);
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<ColPiece>; 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<ColPiece> {
fn sq_mut(&mut self, idx: Square) -> &mut Option<ColPiece> {
&mut self.0[usize::from(idx)]
}
/// 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)]
}
}
@ -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<Index>,
ep_square: Option<Square>,
/// 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<ColPiece> {
fn get_piece(&self, idx: Square) -> Option<ColPiece> {
*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));
}

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
}
}