[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:
parent
481ae70698
commit
7f25548335
10 changed files with 249 additions and 10 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)
|
||||
|
|
60
board/src/piece_sets/counts.rs
Normal file
60
board/src/piece_sets/counts.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
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 {
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod colors;
|
|||
pub mod coordinates;
|
||||
pub mod pieces;
|
||||
pub mod random;
|
||||
pub mod score;
|
||||
pub mod shapes;
|
||||
|
||||
mod macros;
|
||||
|
|
71
core/src/score.rs
Normal file
71
core/src/score.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
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<ScoreInner> for Score {
|
||||
type Output = Score;
|
||||
|
||||
fn mul(self, rhs: ScoreInner) -> Self::Output {
|
||||
Score(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Score> for ScoreInner {
|
||||
type Output = Score;
|
||||
|
||||
fn mul(self, rhs: Score) -> Self::Output {
|
||||
Score(self * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScoreInner> for Score {
|
||||
fn from(value: ScoreInner) -> Self {
|
||||
Score(value)
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
62
position/src/evaluation.rs
Normal file
62
position/src/evaluation.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
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<dyn std::error::Error>> {
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
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;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue