[board, core, position] A simple static evaluation method for scoring positions

Implement a new Evaluator struct that evaluates a Board and returns a score. This
evaluation mechanism uses only a material balance function. It doesn't account
for anything else.

Supporting this, add a Counts struct to the internal piece set structure of a
Board. This struct is responsible for keeping counts of how many pieces of each
shape are on the board for each color. Export a count_piece() method on Board
that returns a count of the number of pieces of a particular color and shape.

Implement a newtype wrapper around i32 called Score that represents the score of
a position in centipawns, i.e. hundredths of a pawn. Add piece values to the
Shape enum.
This commit is contained in:
Eryn Wells 2025-06-20 14:23:57 -07:00
parent 481ae70698
commit 7f25548335
10 changed files with 249 additions and 10 deletions

View file

@ -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 {

View file

@ -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)]

View file

@ -1,8 +1,9 @@
// Eryn Wells <eryn@erynwells.me>
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<Piece> {
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)

View file

@ -0,0 +1,60 @@
// Eryn Wells <eryn@erynwells.me>
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);
}
}