chess_inator/src/lib.rs

528 lines
13 KiB
Rust
Raw Normal View History

#![deny(rust_2018_idioms)]
2024-10-20 11:37:44 -04:00
use std::str::FromStr;
pub mod fen;
2024-09-29 10:43:45 -04:00
pub mod movegen;
const BOARD_WIDTH: usize = 8;
const BOARD_HEIGHT: usize = 8;
const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT;
2024-09-27 20:59:38 -04:00
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
enum Color {
#[default]
2024-09-29 10:43:45 -04:00
White = 0,
Black = 1,
}
const N_COLORS: usize = 2;
2024-09-29 10:43:45 -04:00
impl Color {
/// Return opposite color (does not assign).
pub fn flip(self) -> Self {
match self {
Color::White => Color::Black,
Color::Black => Color::White,
}
}
}
2024-09-29 12:32:11 -04:00
impl From<Color> for char {
fn from(value: Color) -> Self {
match value {
Color::White => 'w',
Color::Black => 'b',
}
}
}
2024-10-04 20:03:19 -04:00
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Piece {
Rook,
Bishop,
Knight,
King,
Queen,
Pawn,
}
const N_PIECES: usize = 6;
struct PieceErr;
/// Color and piece.
#[derive(Debug, Clone, Copy)]
struct ColPiece {
pc: Piece,
col: Color,
}
impl TryFrom<char> for ColPiece {
type Error = PieceErr;
fn try_from(value: char) -> Result<Self, Self::Error> {
let col = if value.is_ascii_uppercase() {
Color::White
} else {
Color::Black
};
let mut lower = value;
lower.make_ascii_lowercase();
Ok(ColPiece {
pc: Piece::try_from(lower)?,
col,
})
}
}
impl From<ColPiece> for char {
fn from(value: ColPiece) -> Self {
let lower = char::from(value.pc);
match value.col {
Color::White => lower.to_ascii_uppercase(),
Color::Black => lower,
}
}
}
impl From<ColPiece> for Color {
fn from(value: ColPiece) -> Self {
value.col
}
}
impl From<ColPiece> for Piece {
fn from(value: ColPiece) -> Self {
value.pc
}
}
impl ColPiece {
/// Convert option of piece to character.
pub fn opt_to_char(opt: Option<Self>) -> char {
match opt {
Some(pc) => pc.into(),
None => '.',
}
}
}
/// Square index newtype.
///
/// A1 is (0, 0) -> 0, A2 is (0, 1) -> 2, and H8 is (7, 7) -> 63.
2024-10-20 11:37:44 -04:00
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2024-09-29 10:43:45 -04:00
struct Square(usize);
2024-09-29 12:32:11 -04:00
#[derive(Debug)]
2024-10-04 23:02:00 -04:00
pub enum SquareError {
OutOfBounds,
2024-09-29 13:47:02 -04:00
InvalidCharacter(char),
}
2024-09-29 10:43:45 -04:00
impl TryFrom<usize> for Square {
2024-09-29 13:47:02 -04:00
type Error = SquareError;
fn try_from(value: usize) -> Result<Self, Self::Error> {
if (0..N_SQUARES).contains(&value) {
2024-09-29 10:43:45 -04:00
Ok(Square(value))
} else {
2024-09-29 13:47:02 -04:00
Err(SquareError::OutOfBounds)
}
}
}
2024-10-20 11:37:44 -04:00
macro_rules! sq_try_from {
($T: ty) => {
impl TryFrom<$T> for Square {
type Error = SquareError;
fn try_from(value: $T) -> Result<Self, Self::Error> {
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);
2024-09-29 10:43:45 -04:00
impl From<Square> for usize {
fn from(value: Square) -> Self {
value.0
}
}
2024-09-29 10:43:45 -04:00
impl Square {
2024-09-29 13:47:02 -04:00
fn from_row_col(r: usize, c: usize) -> Result<Self, SquareError> {
//! Get index of square based on row and column.
let ret = BOARD_WIDTH * r + c;
ret.try_into()
}
2024-10-20 11:37:44 -04:00
fn from_row_col_signed(r: isize, c: isize) -> Result<Self, SquareError> {
let ret = (BOARD_WIDTH as isize) * r + c;
ret.try_into()
}
2024-09-29 12:32:11 -04:00
fn to_row_col(self) -> (usize, usize) {
//! Get row, column from index
let div = self.0 / BOARD_WIDTH;
let rem = self.0 % BOARD_WIDTH;
assert!(div <= 7);
assert!(rem <= 7);
(div, rem)
}
/// Convert square to typical human-readable form (e.g. `e4`).
fn to_algebraic(self) -> String {
let letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
let (row, col) = self.to_row_col();
let rank = (row + 1).to_string();
let file = letters[col];
format!("{file}{rank}")
}
2024-10-20 11:37:44 -04:00
}
impl FromStr for Square {
type Err = SquareError;
2024-09-29 13:47:02 -04:00
/// Convert typical human-readable form (e.g. `e4`) to square index.
2024-10-20 11:37:44 -04:00
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s.as_bytes();
2024-09-29 13:47:02 -04:00
let col = match bytes[0] as char {
'a' => 0,
'b' => 1,
'c' => 2,
'd' => 3,
'e' => 4,
'f' => 5,
'g' => 6,
'h' => 7,
2024-10-06 21:28:11 -04:00
_ => return Err(SquareError::InvalidCharacter(bytes[0] as char)),
2024-09-29 13:47:02 -04:00
};
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))
}
}
2024-09-29 12:32:11 -04:00
}
impl TryFrom<char> for Piece {
type Error = PieceErr;
fn try_from(s: char) -> Result<Self, Self::Error> {
match s {
'r' => Ok(Piece::Rook),
'b' => Ok(Piece::Bishop),
'n' => Ok(Piece::Knight),
'k' => Ok(Piece::King),
'q' => Ok(Piece::Queen),
'p' => Ok(Piece::Pawn),
_ => Err(PieceErr),
}
}
}
impl From<Piece> for char {
fn from(value: Piece) -> Self {
match value {
Piece::Rook => 'r',
Piece::Bishop => 'b',
Piece::Knight => 'n',
Piece::King => 'k',
Piece::Queen => 'q',
Piece::Pawn => 'p',
}
}
}
2024-09-29 10:43:45 -04:00
#[derive(Default, Debug, Clone, Copy)]
struct Bitboard(u64);
impl Bitboard {
2024-09-29 10:43:45 -04:00
pub fn on_idx(&mut self, idx: Square) {
//! Set the square at an index to on.
self.0 |= 1 << usize::from(idx);
}
2024-09-29 10:43:45 -04:00
pub fn off_idx(&mut self, idx: Square) {
//! Set the square at an index to off.
self.0 &= !(1 << usize::from(idx));
}
2024-10-18 17:12:06 -04:00
pub fn is_empty(&self) -> bool {
self.0 == 0
}
}
impl IntoIterator for Bitboard {
type Item = Square;
type IntoIter = BitboardIterator;
fn into_iter(self) -> Self::IntoIter {
BitboardIterator { remaining: self }
}
}
struct BitboardIterator {
remaining: Bitboard,
}
impl Iterator for BitboardIterator {
type Item = Square;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
None
} else {
let next_idx = self.remaining.0.trailing_zeros() as usize;
let sq = Square(next_idx);
self.remaining.off_idx(sq);
Some(sq)
}
}
}
/// Array form board.
///
/// Complements bitboards, notably for "what piece is at this square?" queries.
2024-09-29 10:43:45 -04:00
#[derive(Debug, Clone, Copy)]
struct Mailbox([Option<ColPiece>; N_SQUARES]);
impl Default for Mailbox {
fn default() -> Self {
Mailbox([None; N_SQUARES])
}
}
impl Mailbox {
/// Get mutable reference to square at index.
2024-09-29 10:43:45 -04:00
fn sq_mut(&mut self, idx: Square) -> &mut Option<ColPiece> {
&mut self.0[usize::from(idx)]
}
/// Get non-mutable reference to square at index.
2024-09-29 10:43:45 -04:00
fn sq(&self, idx: Square) -> &Option<ColPiece> {
&self.0[usize::from(idx)]
}
}
/// Piece bitboards and state for one player.
///
/// Default is all empty.
2024-09-29 10:43:45 -04:00
#[derive(Default, Debug, Clone, Copy)]
struct Player {
/// Bitboards for individual pieces. Piece -> locations.
bit: [Bitboard; N_PIECES],
}
impl Player {
2024-10-20 11:37:44 -04:00
/// 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]
}
}
/// Castling rights for one player
2024-09-29 10:43:45 -04:00
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
2024-09-29 12:32:11 -04:00
pub struct CastlePlayer {
/// Kingside
k: bool,
/// Queenside
q: bool,
}
2024-09-29 12:32:11 -04:00
/// Castling rights for both players
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub struct CastleRights([CastlePlayer; N_COLORS]);
impl ToString for CastleRights {
/// Convert to FEN castling rights format.
fn to_string(&self) -> String {
let mut ret = String::with_capacity(4);
for (val, ch) in [
(self.0[Color::White as usize].k, 'K'),
(self.0[Color::White as usize].q, 'Q'),
(self.0[Color::Black as usize].k, 'k'),
(self.0[Color::Black as usize].q, 'q'),
] {
if val {
ret.push(ch)
}
}
if ret.is_empty() {
ret.push('-')
}
ret
}
}
2024-09-29 10:43:45 -04:00
/// Immutable game state, unique to a position.
///
/// Default is empty.
2024-09-29 10:43:45 -04:00
#[derive(Debug, Default, Clone, Copy)]
pub struct BoardState {
/// Player bitboards
players: [Player; N_COLORS],
/// Mailbox (array) board. Location -> piece.
mail: Mailbox,
/// En-passant square.
///
/// (If a pawn moves twice, this is one square in front of the start position.)
2024-09-29 10:43:45 -04:00
ep_square: Option<Square>,
2024-09-27 20:59:38 -04:00
/// Castling rights
2024-09-29 12:32:11 -04:00
castle: CastleRights,
/// Plies since last irreversible (capture, pawn) move
half_moves: usize,
/// Full move counter (incremented after each black turn)
full_moves: usize,
/// Whose turn it is
turn: Color,
}
2024-10-06 21:28:11 -04:00
/// Piece missing where there should be one.
#[derive(Debug)]
struct NoPieceError;
2024-09-29 10:43:45 -04:00
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.
2024-09-29 10:43:45 -04:00
fn set_piece(&mut self, idx: Square, pc: ColPiece) {
let pl = self.pl_mut(pc.col);
2024-10-20 11:37:44 -04:00
pl.board_mut(pc.into()).on_idx(idx);
*self.mail.sq_mut(idx) = Some(pc);
}
2024-10-18 16:24:15 -04:00
/// Delete the piece in a location, and return ("pop") that piece.
2024-10-06 21:28:11 -04:00
///
/// Returns an error if there is no piece in the location.
2024-10-18 16:24:15 -04:00
fn del_piece(&mut self, idx: Square) -> Result<ColPiece, NoPieceError> {
if let Some(pc) = *self.mail.sq_mut(idx) {
let pl = self.pl_mut(pc.col);
2024-10-20 11:37:44 -04:00
pl.board_mut(pc.into()).off_idx(idx);
*self.mail.sq_mut(idx) = None;
2024-10-18 16:24:15 -04:00
Ok(pc)
2024-10-06 21:28:11 -04:00
} else {
Err(NoPieceError)
}
}
2024-10-18 16:24:15 -04:00
fn move_piece(&mut self, src: Square, dest: Square) {
let pc = self.del_piece(src).expect("Move source should have piece.");
self.set_piece(dest, pc);
}
/// Get the piece at a location.
2024-09-29 10:43:45 -04:00
fn get_piece(&self, idx: Square) -> Option<ColPiece> {
*self.mail.sq(idx)
}
2024-09-27 20:59:38 -04:00
/// Maximum amount of moves in the counter to parse before giving up
const MAX_MOVES: usize = 9_999;
}
2024-09-29 10:43:45 -04:00
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 {
2024-09-29 10:43:45 -04:00
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));
}
str += "\n";
}
write!(f, "{}", str)
}
}
2024-10-18 17:12:06 -04:00
#[cfg(test)]
mod tests {
use super::*;
2024-10-20 11:37:44 -04:00
#[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);
}
}
2024-10-18 17:12:06 -04:00
#[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!(
2024-10-20 11:37:44 -04:00
sqr.parse::<Square>().unwrap(),
2024-10-18 17:12:06 -04:00
Square::try_from(idx).unwrap()
);
}
}
#[test]
fn test_bitboard_iteration() {
let indices = [
0usize, 5usize, 17usize, 24usize, 34usize, 39usize, 42usize, 45usize, 49usize, 50usize,
63usize,
];
let mut bitboard = Bitboard::default();
let squares = indices.map(Square);
for sq in squares {
bitboard.on_idx(sq);
}
// ensure that iteration does not consume the board
for _ in 0..=1 {
for (i, sq) in bitboard.into_iter().enumerate() {
assert_eq!(squares[i], sq)
}
}
}
}