[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