Skip to content

Commit

Permalink
Start to implement a Position struct
Browse files Browse the repository at this point in the history
  • Loading branch information
bsamseth committed Nov 10, 2024
1 parent cd7c3e6 commit 9ecf10b
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 3 deletions.
19 changes: 19 additions & 0 deletions goldchess/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ pub enum Color {

impl Color {
pub const ALL: [Color; 2] = [Color::White, Color::Black];
pub const NUM_COLORS: usize = Color::ALL.len();

/// Get the index of the color.
#[must_use]
pub const fn as_index(self) -> usize {
match self {
Color::White => 0,
Color::Black => 1,
}
}
}

impl std::ops::Not for Color {
Expand All @@ -19,3 +29,12 @@ impl std::ops::Not for Color {
}
}
}

impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Color::White => write!(f, "w"),
Color::Black => write!(f, "b"),
}
}
}
2 changes: 2 additions & 0 deletions goldchess/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum Error {
InvalidRank(u8),
#[error("Invalid rank notation: {0}")]
InvalidRankChar(char),
#[error("Invalid FEN notation: {0}")]
InvalidFen(String),
}

/// A specialized [`Result`] type for this crate, used by all fallible functions.
Expand Down
10 changes: 10 additions & 0 deletions goldchess/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ impl File {
pub const F: File = File(5);
pub const G: File = File(6);
pub const H: File = File(7);
pub const ALL: [File; 8] = [
File::A,
File::B,
File::C,
File::D,
File::E,
File::F,
File::G,
File::H,
];
}

impl From<File> for u8 {
Expand Down
2 changes: 2 additions & 0 deletions goldchess/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod error;
mod file;
mod generated_tables;
mod piece;
mod position;
mod rank;
mod square;

Expand All @@ -22,5 +23,6 @@ pub use color::Color;
pub use error::{Error, Result};
pub use file::File;
pub use piece::Piece;
pub use position::Position;
pub use rank::Rank;
pub use square::Square;
30 changes: 30 additions & 0 deletions goldchess/src/piece.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub enum Piece {
}

impl Piece {
/// All possible pieces, in order of their [`Self::as_index`] value.
pub const ALL: [Piece; 6] = [
Piece::Pawn,
Piece::Knight,
Expand All @@ -18,4 +19,33 @@ impl Piece {
Piece::Queen,
Piece::King,
];

/// The number of distinct piece types.
pub const NUM_PIECES: usize = Piece::ALL.len();

/// Get the index of the piece.
#[must_use]
pub const fn as_index(self) -> usize {
match self {
Piece::Pawn => 0,
Piece::Knight => 1,
Piece::Bishop => 2,
Piece::Rook => 3,
Piece::Queen => 4,
Piece::King => 5,
}
}

/// Get the (upper case) character representation of the piece.
#[must_use]
pub const fn as_char(self) -> char {
match self {
Piece::Pawn => 'P',
Piece::Knight => 'N',
Piece::Bishop => 'B',
Piece::Rook => 'R',
Piece::Queen => 'Q',
Piece::King => 'K',
}
}
}
244 changes: 244 additions & 0 deletions goldchess/src/position.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
use std::str::FromStr;

use crate::{Bitboard, CastleRights, Color, Error, File, Piece, Rank, Square};

#[derive(Debug, Clone)]
pub struct Position {
pieces: [Bitboard; Piece::NUM_PIECES],
color_combined: [Bitboard; Color::NUM_COLORS],
side_to_move: Color,
castle_rights: [CastleRights; Color::NUM_COLORS],
en_passant: Option<Square>,
halfmove_clock: u8,
}

impl Position {
#[must_use]
pub fn piece_on(&self, sq: Square) -> Option<Piece> {
Piece::ALL
.into_iter()
.find(|&piece| self.pieces[piece.as_index()] & sq != Bitboard::EMPTY)
}

#[must_use]
pub fn color_on(&self, sq: Square) -> Option<Color> {
Color::ALL
.into_iter()
.find(|&color| self.color_combined[color.as_index()] & sq != Bitboard::EMPTY)
}

pub const STARTING_POSITION_FEN: &str =
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
}

impl Default for Position {
fn default() -> Self {
Position::from_str(Position::STARTING_POSITION_FEN).unwrap()
}
}

impl FromStr for Position {
type Err = Error;

fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut board = [None; Square::NUM_SQUARES];
let invalid_fen = || Error::InvalidFen(value.to_string());

let tokens: Vec<&str> = value.split(' ').collect();
if tokens.len() < 4 {
return Err(invalid_fen());
}

let pieces = tokens[0];
let side = tokens[1];
let castles = tokens[2];
let ep = tokens[3];
let halfmove = tokens[4];
let mut cur_rank = Rank::R8;
let mut cur_file = File::A;

for x in pieces.chars() {
match x {
'/' => {
cur_rank = cur_rank
.down()
.ok_or_else(|| Error::InvalidFen(value.to_string()))?;
cur_file = File::A;
}
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' => {
cur_file = File::new(
u8::try_from(cur_file.0 as usize + (x as usize) - ('0' as usize))
.map_err(|_| invalid_fen())?
% 8,
)
.map_err(|_| invalid_fen())?;
}
x => {
let (piece, color) = match x {
'r' => (Piece::Rook, Color::Black),
'R' => (Piece::Rook, Color::White),
'n' => (Piece::Knight, Color::Black),
'N' => (Piece::Knight, Color::White),
'b' => (Piece::Bishop, Color::Black),
'B' => (Piece::Bishop, Color::White),
'q' => (Piece::Queen, Color::Black),
'Q' => (Piece::Queen, Color::White),
'k' => (Piece::King, Color::Black),
'K' => (Piece::King, Color::White),
'p' => (Piece::Pawn, Color::Black),
'P' => (Piece::Pawn, Color::White),
_ => {
return Err(invalid_fen());
}
};
board[Square::make_square(cur_rank, cur_file).as_index()] =
Some((piece, color));
cur_file = cur_file.right().unwrap_or(File::A);
}
}
}
let side_to_move = match side {
"w" | "W" => Color::White,
"b" | "B" => Color::Black,
_ => {
return Err(invalid_fen());
}
};

let mut castle_rights = [CastleRights::None; Color::NUM_COLORS];
if castles.contains('K') && castles.contains('Q') {
castle_rights[Color::White.as_index()] = CastleRights::Both;
} else if castles.contains('K') {
castle_rights[Color::White.as_index()] = CastleRights::KingSide;
} else if castles.contains('Q') {
castle_rights[Color::White.as_index()] = CastleRights::QueenSide;
}

if castles.contains('k') && castles.contains('q') {
castle_rights[Color::Black.as_index()] = CastleRights::Both;
} else if castles.contains('k') {
castle_rights[Color::Black.as_index()] = CastleRights::KingSide;
} else if castles.contains('q') {
castle_rights[Color::Black.as_index()] = CastleRights::QueenSide;
}

let ep_square = Square::from_str(ep).ok();

let halfmove_clock = halfmove.parse::<u8>().map_err(|_| invalid_fen())?;

let mut pieces = [Bitboard(0); Piece::NUM_PIECES];
let mut color_combined = [Bitboard(0); Color::NUM_COLORS];
for (&piece, sq) in board.iter().zip(Square::ALL_SQUARES) {
if let Some((piece, color)) = piece {
pieces[piece.as_index()] |= sq;
color_combined[color.as_index()] |= sq;
}
}

Ok(Position {
pieces,
color_combined,
side_to_move,
castle_rights,
en_passant: ep_square,
halfmove_clock,
})
}
}

impl std::fmt::Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for rank in Rank::ALL.into_iter().rev() {
let mut empty = 0;
for file in File::ALL {
let sq = Square::make_square(rank, file);
match self.piece_on(sq) {
None => empty += 1,
Some(piece) => {
if empty > 0 {
write!(f, "{empty}")?;
empty = 0;
}
let color = self.color_on(sq).unwrap();
let c = piece.as_char();
write!(
f,
"{}",
if color == Color::Black {
c.to_ascii_lowercase()
} else {
c
}
)?;
}
}
}
if empty > 0 {
write!(f, "{empty}")?;
}
if rank != Rank::R1 {
write!(f, "/")?;
}
}

write!(f, " {} ", self.side_to_move)?;

match self.castle_rights {
[CastleRights::None, CastleRights::None] => write!(f, "-")?,
[white, black] => {
match white {
CastleRights::KingSide => write!(f, "K")?,
CastleRights::QueenSide => write!(f, "Q")?,
CastleRights::Both => write!(f, "KQ")?,
CastleRights::None => {}
}

match black {
CastleRights::KingSide => write!(f, "k")?,
CastleRights::QueenSide => write!(f, "q")?,
CastleRights::Both => write!(f, "kq")?,
CastleRights::None => {}
}
}
}

if let Some(ep) = self.en_passant {
write!(f, " {ep}")?;
} else {
write!(f, " -")?;
}

write!(f, " {}", self.halfmove_clock)?;
write!(f, " 1") // Full move clock, not stored here so just write 1 to be FEN-compliant.
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_from_str() {
for fen in [
"2n1k3/8/8/8/8/8/8/2N1K3 w - - 0 1",
"5B2/6P1/1p6/8/1N6/kP6/2K5/8 w - - 0 1",
"6R1/P2k4/r7/5N1P/r7/p7/7K/8 w - - 0 1",
"7K/8/k1P5/7p/8/8/8/8 w - - 0 1",
"7k/8/8/8/8/8/8/7K w - - 0 1",
"8/5k2/3p4/1pPPp2p/pP2Pp1P/P4P1K/8/8 b - b6 99 1",
"8/8/7p/3KNN1k/2p4p/8/3P2p1/8 w - - 0 1",
"r4rk1/1pp2ppp/3b4/3P4/3p4/3B4/1PP2PPP/R4RK1 b - - 0 1",
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b - - 1 1",
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 1",
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b Kkq - 1 1",
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R w kq - 10 1",
"rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 1",
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
] {
let pos = Position::from_str(fen).unwrap();
assert_eq!(pos.to_string(), fen);
println!("{pos}");
}
}
}
10 changes: 10 additions & 0 deletions goldchess/src/rank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ impl Rank {
pub const R6: Rank = Rank(5);
pub const R7: Rank = Rank(6);
pub const R8: Rank = Rank(7);
pub const ALL: [Rank; 8] = [
Rank::R1,
Rank::R2,
Rank::R3,
Rank::R4,
Rank::R5,
Rank::R6,
Rank::R7,
Rank::R8,
];
}

impl From<Rank> for u8 {
Expand Down
Loading

0 comments on commit 9ecf10b

Please sign in to comment.