diff --git a/board/src/board.rs b/board/src/board.rs index 6c56346..338d4aa 100644 --- a/board/src/board.rs +++ b/board/src/board.rs @@ -3,7 +3,7 @@ use crate::{ CastleRights, PieceSet, display::DiagramFormatter, - piece_sets::{PlacePieceError, PlacePieceStrategy}, + piece_sets::{Counter, PlacePieceError, PlacePieceStrategy}, zobrist::{ZobristHash, ZobristState}, }; use chessfriend_bitboard::BitBoard; @@ -219,6 +219,11 @@ impl Board { removed_piece } + + #[must_use] + pub fn count_piece(&self, piece: &Piece) -> Counter { + self.pieces.count(piece) + } } impl Board { diff --git a/board/src/fen.rs b/board/src/fen.rs index 65c468a..a44b735 100644 --- a/board/src/fen.rs +++ b/board/src/fen.rs @@ -9,9 +9,10 @@ use thiserror::Error; #[macro_export] macro_rules! fen { - ($fen_string:literal) => { + ($fen_string:literal) => {{ + use $crate::fen::FromFenStr; Board::from_fen_str($fen_string) - }; + }}; } #[derive(Clone, Debug, Error, Eq, PartialEq)] diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 063937d..316c1d8 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -1,8 +1,9 @@ // Eryn Wells +mod counts; mod mailbox; -use self::mailbox::Mailbox; +use self::{counts::Counts, mailbox::Mailbox}; use chessfriend_bitboard::{BitBoard, IterationDirection}; use chessfriend_core::{Color, Piece, Shape, Square}; use std::{ @@ -11,6 +12,8 @@ use std::{ }; use thiserror::Error; +pub(crate) use counts::Counter; + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum PlacePieceStrategy { #[default] @@ -29,6 +32,7 @@ pub enum PlacePieceError { #[derive(Clone, Debug, Default, Eq)] pub struct PieceSet { mailbox: Mailbox, + counts: Counts, color_occupancy: [BitBoard; Color::NUM], shape_occupancy: [BitBoard; Shape::NUM], } @@ -36,18 +40,21 @@ pub struct PieceSet { impl PieceSet { pub(crate) fn new(pieces: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self { let mut mailbox = Mailbox::default(); + let mut counts = Counts::default(); let mut color_occupancy: [BitBoard; Color::NUM] = Default::default(); let mut shape_occupancy: [BitBoard; Shape::NUM] = Default::default(); - for (color_index, color) in Color::iter().enumerate() { + for (color_index, color) in Color::into_iter().enumerate() { for (shape_index, shape) in Shape::into_iter().enumerate() { let bitboard = pieces[color_index][shape_index]; color_occupancy[color_index] |= bitboard; shape_occupancy[shape_index] |= bitboard; + counts.increment(color, shape); + for square in bitboard.occupied_squares(&IterationDirection::default()) { - let piece = Piece::new(*color, shape); + let piece = Piece::new(color, shape); mailbox.set(piece, square); } } @@ -55,6 +62,7 @@ impl PieceSet { Self { mailbox, + counts, color_occupancy, shape_occupancy, } @@ -94,6 +102,10 @@ impl PieceSet { self.mailbox.get(square) } + pub(crate) fn count(&self, piece: &Piece) -> Counter { + self.counts.get(piece.color, piece.shape) + } + // TODO: Rename this. Maybe get_all() is better? pub(crate) fn find_pieces(&self, piece: Piece) -> BitBoard { let color_occupancy = self.color_occupancy[piece.color as usize]; @@ -120,6 +132,7 @@ impl PieceSet { self.color_occupancy[color as usize].set(square); self.shape_occupancy[shape as usize].set(square); + self.counts.increment(color, shape); self.mailbox.set(piece, square); Ok(existing_piece) @@ -127,8 +140,12 @@ impl PieceSet { pub(crate) fn remove(&mut self, square: Square) -> Option { if let Some(piece) = self.mailbox.get(square) { - self.color_occupancy[piece.color as usize].clear(square); - self.shape_occupancy[piece.shape as usize].clear(square); + let color_index = piece.color as usize; + let shape_index = piece.shape as usize; + + self.color_occupancy[color_index].clear(square); + self.shape_occupancy[shape_index].clear(square); + self.counts.decrement(piece.color, piece.shape); self.mailbox.remove(square); Some(piece) diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs new file mode 100644 index 0000000..effbbe0 --- /dev/null +++ b/board/src/piece_sets/counts.rs @@ -0,0 +1,60 @@ +// Eryn Wells + +use chessfriend_core::{Color, Shape, Square}; + +pub(crate) type Counter = u8; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(super) struct Counts([[Counter; Shape::NUM]; Color::NUM]); + +impl Counts { + pub fn get(&self, color: Color, shape: Shape) -> Counter { + self.0[color as usize][shape as usize] + } + + pub fn increment(&mut self, color: Color, shape: Shape) { + #[allow(clippy::cast_possible_truncation)] + const SQUARE_NUM: u8 = Square::NUM as u8; + + let updated_value = self.0[color as usize][shape as usize] + 1; + if updated_value <= SQUARE_NUM { + self.0[color as usize][shape as usize] = updated_value; + } else { + unreachable!("piece count for {color} {shape} overflowed"); + } + } + + pub fn decrement(&mut self, color: Color, shape: Shape) { + let count = self.0[color as usize][shape as usize]; + if let Some(updated_count) = count.checked_sub(1) { + self.0[color as usize][shape as usize] = updated_count; + } else { + unreachable!("piece count for {color} {shape} underflowed"); + } + } + + #[cfg(test)] + fn set(&mut self, color: Color, shape: Shape, value: u8) { + self.0[color as usize][shape as usize] = value; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "underflowed")] + fn underflow() { + let mut counts = Counts::default(); + counts.decrement(Color::White, Shape::Queen); + } + + #[test] + #[should_panic(expected = "overflowed")] + fn overflow() { + let mut counts = Counts::default(); + counts.set(Color::White, Shape::Queen, 64); + counts.increment(Color::White, Shape::Queen); + } +} diff --git a/core/src/colors.rs b/core/src/colors.rs index 5cf633b..ccf62a7 100644 --- a/core/src/colors.rs +++ b/core/src/colors.rs @@ -1,6 +1,6 @@ // Eryn Wells -use crate::Direction; +use crate::{Direction, score::ScoreInner}; use thiserror::Error; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] @@ -56,6 +56,14 @@ impl Color { Color::Black => "black", } } + + #[must_use] + pub const fn score_factor(self) -> ScoreInner { + match self { + Color::White => 1, + Color::Black => -1, + } + } } impl std::fmt::Display for Color { diff --git a/core/src/lib.rs b/core/src/lib.rs index 0638410..f298a81 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -4,6 +4,7 @@ pub mod colors; pub mod coordinates; pub mod pieces; pub mod random; +pub mod score; pub mod shapes; mod macros; diff --git a/core/src/score.rs b/core/src/score.rs new file mode 100644 index 0000000..eee49f6 --- /dev/null +++ b/core/src/score.rs @@ -0,0 +1,71 @@ +// Eryn Wells + +use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; + +pub(crate) type ScoreInner = i32; + +/// A score for a position in centipawns. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Score(ScoreInner); + +impl Score { + #[must_use] + pub const fn zero() -> Self { + Self(0) + } + + #[must_use] + pub const fn new(value: ScoreInner) -> Self { + Self(value) + } +} + +impl Add for Score { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Score(self.0 + rhs.0) + } +} + +impl AddAssign for Score { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + +impl Sub for Score { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Score(self.0 - rhs.0) + } +} + +impl SubAssign for Score { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } +} + +impl Mul for Score { + type Output = Score; + + fn mul(self, rhs: ScoreInner) -> Self::Output { + Score(self.0 * rhs) + } +} + +impl Mul for ScoreInner { + type Output = Score; + + fn mul(self, rhs: Score) -> Self::Output { + Score(self * rhs.0) + } +} + +impl From for Score { + fn from(value: ScoreInner) -> Self { + Score(value) + } +} diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 2c6b7e9..8184f1d 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -3,6 +3,8 @@ use std::{array, fmt, slice, str::FromStr}; use thiserror::Error; +use crate::score::Score; + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { Pawn = 0, @@ -71,6 +73,17 @@ impl Shape { pub fn is_promotable(&self) -> bool { Self::PROMOTABLE_SHAPES.contains(self) } + + #[must_use] + pub fn score(self) -> Score { + match self { + Shape::Pawn => Score::new(100), + Shape::Knight | Shape::Bishop => Score::new(300), + Shape::Rook => Score::new(500), + Shape::Queen => Score::new(900), + Shape::King => Score::new(20000), + } + } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs new file mode 100644 index 0000000..883398b --- /dev/null +++ b/position/src/evaluation.rs @@ -0,0 +1,62 @@ +// Eryn Wells + +use chessfriend_board::Board; +use chessfriend_core::{Color, Piece, Shape, score::Score}; + +struct Evaluator; + +impl Evaluator { + pub fn evaluate_symmetric_unwrapped(board: &Board, color: Color) -> Score { + let material_balance = Self::material_balance(board, color); + + let to_move_factor = color.score_factor(); + + to_move_factor * material_balance + } + + /// Evaluate a board using the symmetric evaluation algorithm defined by + /// Claude Shannon. + fn material_balance(board: &Board, color: Color) -> Score { + let other_color = color.other(); + + Shape::into_iter().fold(Score::zero(), |acc, shape| { + let (active_pieces, other_pieces) = ( + board.count_piece(&Piece::new(color, shape)) as i32, + board.count_piece(&Piece::new(other_color, shape)) as i32, + ); + + let factor = shape.score() * (active_pieces - other_pieces); + + acc + factor + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chessfriend_board::fen; + + #[test] + fn pawn_material_balance() -> Result<(), Box> { + let board = fen!("8/8/8/8/8/3P4/8/8 w - - 0 1")?; + assert_eq!( + Evaluator::material_balance(&board, Color::White), + 100i32.into() + ); + + let board = fen!("8/8/3p4/8/8/3P4/8/8 w - - 0 1")?; + assert_eq!(Evaluator::material_balance(&board, Color::White), 0.into()); + + Ok(()) + } + + #[test] + fn starting_position_is_even() { + let board = Board::starting(None); + assert_eq!( + Evaluator::evaluate_symmetric_unwrapped(&board, Color::White), + Evaluator::evaluate_symmetric_unwrapped(&board, Color::Black) + ); + } +} diff --git a/position/src/lib.rs b/position/src/lib.rs index d0a1ec5..7ccee47 100644 --- a/position/src/lib.rs +++ b/position/src/lib.rs @@ -1,11 +1,12 @@ // Eryn Wells +mod evaluation; mod position; #[macro_use] mod macros; -pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy}; +pub use chessfriend_board::{PlacePieceError, PlacePieceStrategy, fen}; pub use chessfriend_moves::{GeneratedMove, ValidateMove}; pub use position::Position;