Skip to content

Commit

Permalink
Decouple eval from position struct (#280)
Browse files Browse the repository at this point in the history
* Put more stuff in `HistoryIndex`

* Decouple Eval state from Position struct

Just getting passed in as a negamax parameter atm, but could also see it
getting passed around through the search stack, or some other mechanism,
further down the line.

bench 7142602
  • Loading branch information
sroelants authored Aug 13, 2024
1 parent 8201a0c commit 1a40a99
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 96 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ default-members = ["simbelmyne"]
opt-level = 3
codegen-units = 1
lto = "fat"
debug = 0
debug = 1
panic = "abort"
strip = true
strip = false
37 changes: 37 additions & 0 deletions simbelmyne/src/evaluate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ pub mod terms;
pub mod util;
mod piece_square_tables;

use crate::history_tables::history::HistoryIndex;
use crate::s;

use chess::bitboard::Bitboard;
use chess::board::Board;
use chess::movegen::castling::CastleType;
use chess::movegen::moves::Move;
use chess::piece::Piece;
use chess::square::Square;
use chess::piece::PieceType;
Expand Down Expand Up @@ -216,6 +219,40 @@ impl Eval {
if board.current.is_white() { score } else { -score }
}

pub fn play_move(&self, idx: HistoryIndex, board: &Board) -> Self {
let mut new_score = *self;
let HistoryIndex { moved, captured, mv } = idx;
let us = moved.color();

if mv == Move::NULL {
return new_score;
}

// Remove any captured pieces
if let Some(captured) = captured {
new_score.remove(captured, mv.get_capture_sq(), &board);
}

// Update the moved piece
if idx.mv.is_promotion() {
new_score.remove(moved, mv.src(), &board);

let promo_piece = Piece::new(mv.get_promo_type().unwrap(), us);
new_score.add(promo_piece, mv.tgt(), &board);
} else {
new_score.update(moved, mv.src(), mv.tgt(), &board);
}

if mv.is_castle() {
let ctype = CastleType::from_move(mv).unwrap();
let rook_move = ctype.rook_move();
let rook = Piece::new(PieceType::Rook, us);
new_score.update(rook, rook_move.src(), rook_move.tgt(), &board);
}

new_score
}

/// Update the Eval by adding a piece to it
pub fn add(&mut self, piece: Piece, sq: Square, board: &Board) {
self.game_phase += Self::phase_value(piece);
Expand Down
30 changes: 0 additions & 30 deletions simbelmyne/src/evaluate/tuner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,33 +405,3 @@ impl<const N: usize> From<[Score; N]> for EvalWeights {
bytemuck::cast::<[S; N], EvalWeights>(weights)
}
}

////////////////////////////////////////////////////////////////////////////////
//
// Tests
//
////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use super::*;
use crate::{position::Position, tests::TEST_POSITIONS};
use tuner::evaluate_components;

#[test]
#[ignore = "Broken test, try and make less flaky in the future"]
fn default_evalweights_evaluation_returns_same_value() {
for fen in TEST_POSITIONS {
let board: Board = fen.parse().unwrap();
let weights = EvalWeights::default().weights();
let components = EvalWeights::components(&board);
let weight_eval = evaluate_components(&weights, &components, board.phase());

let position = Position::new(board);
let classical_eval = position.score.total(&board);

println!("{fen}\nClassical eval: {classical_eval}, EvalWeights eval: {weight_eval}\n\n");
// Allow for slight discrepancies because of rounding differences
assert!(f32::abs(weight_eval - classical_eval as f32) <= 5.0)
}
}
}
4 changes: 2 additions & 2 deletions simbelmyne/src/history_tables/conthist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ impl Index<HistoryIndex> for ContHist {
type Output = HistoryTable;

fn index(&self, index: HistoryIndex) -> &Self::Output {
&self.table[index.1][index.0]
&self.table[index.moved][index.tgt()]
}
}

impl IndexMut<HistoryIndex> for ContHist {
fn index_mut(&mut self, index: HistoryIndex) -> &mut Self::Output {
&mut self.table[index.1][index.0]
&mut self.table[index.moved][index.tgt()]
}
}
4 changes: 2 additions & 2 deletions simbelmyne/src/history_tables/countermoves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ impl Index<HistoryIndex> for CountermoveTable {
type Output = Option<Move>;

fn index(&self, index: HistoryIndex) -> &Self::Output {
&self.scores[index.1][index.0]
&self.scores[index.moved][index.tgt()]
}
}

impl IndexMut<HistoryIndex> for CountermoveTable {
fn index_mut(&mut self, index: HistoryIndex) -> &mut Self::Output {
&mut self.scores[index.1][index.0]
&mut self.scores[index.moved][index.tgt()]
}
}
40 changes: 33 additions & 7 deletions simbelmyne/src/history_tables/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ impl Index<HistoryIndex> for HistoryTable {
type Output = HistoryScore;

fn index(&self, index: HistoryIndex) -> &Self::Output {
&self.scores[index.1][index.0]
&self.scores[index.moved][index.tgt()]
}
}

impl IndexMut<HistoryIndex> for HistoryTable {
fn index_mut(&mut self, index: HistoryIndex) -> &mut Self::Output {
&mut self.scores[index.1][index.0]
&mut self.scores[index.moved][index.tgt()]
}
}

Expand All @@ -69,23 +69,49 @@ impl IndexMut<HistoryIndex> for HistoryTable {
/// A History index is a convenient wrapper used to index into a History table,
/// comprising of a Piece and a destination Square
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct HistoryIndex(pub(super) Square, pub(super) Piece);
pub struct HistoryIndex{
pub mv: Move,
pub moved: Piece,
pub captured: Option<Piece>,
}

impl HistoryIndex {
pub fn new(board: &Board, mv: Move) -> Self {
let square = mv.tgt();
let piece = board.get_at(mv.src()).unwrap();
let captured = if mv.is_capture() {
board.get_at(mv.get_capture_sq())
} else {
None
};

Self(square, piece)
Self {
mv,
moved: board.get_at(mv.src()).unwrap(),
captured,
}
}
}

impl Default for HistoryIndex {
fn default() -> Self {
Self(Square::A1, Piece::WP)
Self {
mv: Move::NULL,
moved: Piece::WP,
captured: None,
}
}
}

impl HistoryIndex {
pub fn src(&self) -> Square {
self.mv.src()
}

pub fn tgt(&self) -> Square {
self.mv.tgt()
}
}


////////////////////////////////////////////////////////////////////////////////
//
// History score
Expand Down
23 changes: 1 addition & 22 deletions simbelmyne/src/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use arrayvec::ArrayVec;
use chess::{board::Board, movegen::{castling::CastleType, moves::{BareMove, Move}}, piece::Piece};
use crate::{evaluate::Eval, zobrist::ZHash};
use crate::zobrist::ZHash;

// We don't ever expect to exceed 100 entries, because that would be a draw.
const HIST_SIZE: usize = 100;
Expand All @@ -18,9 +18,6 @@ pub struct Position {
/// The board associated with the position.
pub board: Board,

/// The score object associated with the position.
pub score: Eval,

/// The Zobrist hash of the current board
pub hash: ZHash,

Expand All @@ -37,7 +34,6 @@ impl Position {
pub fn new(board: Board) -> Self {
Position {
board,
score: Eval::new(&board),
hash: ZHash::from(board),
pawn_hash: ZHash::pawn_hash(&board),
history: ArrayVec::new(),
Expand Down Expand Up @@ -66,7 +62,6 @@ impl Position {

/// Play a move and update the board, scores and hashes accordingly.
pub fn play_move(&self, mv: Move) -> Self {
let mut new_score = self.score;
let mut new_history = self.history.clone();

// Update board
Expand Down Expand Up @@ -117,7 +112,6 @@ impl Position {
if mv == Move::NULL {
return Self {
board: new_board,
score: new_score,
hash: new_hash,
pawn_hash: self.pawn_hash,
history: new_history
Expand All @@ -135,7 +129,6 @@ impl Position {
let captured = self.board.get_at(captured_sq)
.expect("Move is a capture, so must have piece on target");

new_score.remove(captured, captured_sq, &new_board);
new_hash.toggle_piece(captured, captured_sq);

if captured.is_pawn() {
Expand All @@ -156,14 +149,6 @@ impl Position {
let new_piece = new_board.piece_list[mv.tgt()]
.expect("The target square of a move is occupied after playing");

// Update the score
if mv.is_promotion() {
new_score.remove(old_piece, mv.src(), &new_board);
new_score.add(new_piece, mv.tgt(), &new_board);
} else {
new_score.update(old_piece, mv.src(), mv.tgt(), &new_board);
}

// Update the hash
new_hash.toggle_piece(old_piece, mv.src());
new_hash.toggle_piece(new_piece, mv.tgt());
Expand All @@ -189,9 +174,6 @@ impl Position {
let rook = self.board.piece_list[rook_move.src()]
.expect("We know there is a rook at the starting square");

// Update the score
new_score.update(rook, rook_move.src(), rook_move.tgt(), &new_board);

// Update the hash
new_hash.toggle_piece(rook, rook_move.src());
new_hash.toggle_piece(rook, rook_move.tgt());
Expand Down Expand Up @@ -220,15 +202,13 @@ impl Position {

Self {
board: new_board,
score: new_score,
hash: new_hash,
pawn_hash: new_pawn_hash,
history: new_history,
}
}

pub fn play_null_move(&self) -> Self {
let new_score = self.score;
let new_history = ArrayVec::new();

// Update board
Expand All @@ -252,7 +232,6 @@ impl Position {

Self {
board: new_board,
score: new_score,
hash: new_hash,
pawn_hash: self.pawn_hash,
history: new_history
Expand Down
2 changes: 2 additions & 0 deletions simbelmyne/src/search/aspiration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! The hope, as always in these things, is that the score is stable enough that
//! re-searches are minimal, and the time we save in the best-case scenario
//! more than compensates for the odd re-search.
use crate::evaluate::Eval;
use crate::history_tables::pv::PVTable;
use crate::position::Position;
use crate::evaluate::Score;
Expand Down Expand Up @@ -52,6 +53,7 @@ impl Position {
tt,
pv,
search,
Eval::new(&self.board),
false
);

Expand Down
Loading

0 comments on commit 1a40a99

Please sign in to comment.