Compare commits
7 Commits
ffc2671c4a
...
093dc51eed
Author | SHA1 | Date | |
---|---|---|---|
093dc51eed | |||
e06d614885 | |||
955c9f1f5e | |||
cac74a46cd | |||
db365c124e | |||
a6a7b33553 | |||
8137cbc2fe |
@ -9,8 +9,9 @@ Features:
|
||||
- Tapered midgame-endgame evaluation
|
||||
- UCI compatibility
|
||||
- Iterative deepening
|
||||
- Time management
|
||||
- Transposition table (Zobrist hashing)
|
||||
- Currently only stores best move.
|
||||
- Quiescence search
|
||||
|
||||
## instructions
|
||||
|
||||
|
261
src/lib.rs
261
src/lib.rs
@ -449,6 +449,127 @@ impl Display for CastleRights {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ring-buffer pointer that will never point outside the buffer.
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
struct RingPtr<const N: usize>(usize);
|
||||
|
||||
impl<const N: usize> From<RingPtr<N>> for usize {
|
||||
fn from(value: RingPtr<N>) -> Self {
|
||||
debug_assert!((0..N).contains(&value.0));
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> std::ops::Add<usize> for RingPtr<N> {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: usize) -> Self::Output {
|
||||
Self((self.0 + rhs) % N)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> std::ops::Sub<usize> for RingPtr<N> {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: usize) -> Self::Output {
|
||||
Self((self.0 + N - rhs) % N)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> std::ops::AddAssign<usize> for RingPtr<N> {
|
||||
fn add_assign(&mut self, rhs: usize) {
|
||||
self.0 = (self.0 + rhs) % N;
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> std::ops::SubAssign<usize> for RingPtr<N> {
|
||||
fn sub_assign(&mut self, rhs: usize) {
|
||||
self.0 = (self.0 + N - rhs) % N;
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for RingPtr<N> {
|
||||
fn default() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> RingPtr<N> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ringptr_tests {
|
||||
use super::*;
|
||||
|
||||
/// ring buffer pointer behaviour
|
||||
#[test]
|
||||
fn test_ringptr() {
|
||||
let ptr_start: RingPtr<3> = RingPtr::default();
|
||||
|
||||
let ptr: RingPtr<3> = RingPtr::default() + 3;
|
||||
assert_eq!(ptr, ptr_start);
|
||||
|
||||
let ptr2: RingPtr<3> = RingPtr::default() + 2;
|
||||
assert_eq!(ptr2, ptr_start - 1);
|
||||
assert_eq!(ptr2, ptr_start + 2);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ring-buffer of previously seen hashes, used to avoid draw by repetition.
|
||||
///
|
||||
/// Only stores at most `HISTORY_SIZE` plies, since most cases of repetition happen recently.
|
||||
/// Technically, it should be 100 plies because of the 50-move rule.
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
struct BoardHistory {
|
||||
hashes: [Zobrist; HISTORY_SIZE],
|
||||
/// Index of the start of the history in the buffer
|
||||
ptr_start: RingPtr<HISTORY_SIZE>,
|
||||
/// Index one-past-the-end of the history in the buffer
|
||||
ptr_end: RingPtr<HISTORY_SIZE>,
|
||||
}
|
||||
|
||||
impl PartialEq for BoardHistory {
|
||||
/// Always equal, since comparing two boards with different histories shouldn't matter.
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for BoardHistory {}
|
||||
|
||||
/// Size in plies of the board history.
|
||||
///
|
||||
/// Actual capacity is one less than this.
|
||||
const HISTORY_SIZE: usize = 10;
|
||||
|
||||
impl BoardHistory {
|
||||
/// Counts occurences of this hash in the history.
|
||||
fn count(&self, hash: Zobrist) -> usize {
|
||||
let mut ans = 0;
|
||||
|
||||
let mut i = self.ptr_start;
|
||||
while i != self.ptr_end {
|
||||
if self.hashes[usize::from(i)] == hash {
|
||||
ans += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
ans
|
||||
}
|
||||
|
||||
/// Add (push) hash to history.
|
||||
fn push(&mut self, hash: Zobrist) {
|
||||
self.hashes[usize::from(self.ptr_end)] = hash;
|
||||
|
||||
self.ptr_end += 1;
|
||||
|
||||
// replace old entries
|
||||
if self.ptr_end == self.ptr_start {
|
||||
self.ptr_start += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Game state, describes a position.
|
||||
///
|
||||
/// Default is empty.
|
||||
@ -485,6 +606,9 @@ pub struct Board {
|
||||
|
||||
/// Last captured square
|
||||
recap_sq: Option<Square>,
|
||||
|
||||
/// History of recent hashes to avoid repetition draws.
|
||||
history: BoardHistory,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
@ -493,6 +617,11 @@ impl Board {
|
||||
Board::from_fen(START_POSITION).unwrap()
|
||||
}
|
||||
|
||||
/// Save the current position's hash in the history.
|
||||
pub fn push_history(&mut self) {
|
||||
self.history.push(self.zobrist);
|
||||
}
|
||||
|
||||
/// Get iterator over all squares.
|
||||
pub fn squares() -> impl Iterator<Item = Square> {
|
||||
(0..N_SQUARES).map(Square::try_from).map(|x| x.unwrap())
|
||||
@ -564,13 +693,11 @@ impl Board {
|
||||
turn: self.turn.flip(),
|
||||
half_moves: self.half_moves,
|
||||
full_moves: self.full_moves,
|
||||
players: Default::default(),
|
||||
mail: Default::default(),
|
||||
ep_square: self.ep_square.map(|sq| sq.mirror_vert()),
|
||||
castle: CastleRights(self.castle.0),
|
||||
eval: Default::default(),
|
||||
zobrist: Zobrist::default(),
|
||||
recap_sq: self.recap_sq.map(|sq| sq.mirror_vert()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
new_board.castle.0.reverse();
|
||||
@ -672,6 +799,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use fen::FromFen;
|
||||
use movegen::{FromUCIAlgebraic, Move, ToUCIAlgebraic};
|
||||
|
||||
#[test]
|
||||
fn test_square_casts() {
|
||||
@ -799,4 +927,131 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_history() {
|
||||
let board = Board::starting_pos();
|
||||
|
||||
let mut history = BoardHistory::default();
|
||||
for _ in 0..(HISTORY_SIZE + 15) {
|
||||
history.push(board.zobrist);
|
||||
}
|
||||
|
||||
assert_eq!(history.count(board.zobrist), HISTORY_SIZE - 1);
|
||||
|
||||
let board_empty = Board::default();
|
||||
history.push(board_empty.zobrist);
|
||||
|
||||
assert_eq!(history.count(board.zobrist), HISTORY_SIZE - 2);
|
||||
assert_eq!(history.count(board_empty.zobrist), 1);
|
||||
|
||||
for _ in 0..3 {
|
||||
history.push(board_empty.zobrist);
|
||||
}
|
||||
|
||||
assert_eq!(history.count(board_empty.zobrist), 4);
|
||||
assert_eq!(history.count(board.zobrist), HISTORY_SIZE - 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repetition_detection() {
|
||||
let mut board = Board::starting_pos();
|
||||
|
||||
let mvs = "b1c3 b8c6 c3b1 c6b8"
|
||||
.split_whitespace()
|
||||
.map(Move::from_uci_algebraic)
|
||||
.map(|x| x.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for _ in 0..2 {
|
||||
for mv in &mvs {
|
||||
board.push_history();
|
||||
let _ = mv.make(&mut board);
|
||||
}
|
||||
}
|
||||
|
||||
// this is the third occurence, but beforehand there are two occurences
|
||||
assert_eq!(board.history.count(board.zobrist), 2);
|
||||
}
|
||||
|
||||
/// engine should take advantage of the three time repetition rule
|
||||
#[test]
|
||||
fn find_repetition() {
|
||||
use eval::EvalInt;
|
||||
use search::{best_line, EngineState, SearchConfig, TranspositionTable};
|
||||
let mut board = Board::from_fen("qqqp4/pkpp4/8/8/8/8/8/K7 b - - 0 1").unwrap();
|
||||
|
||||
let mvs = "b7b6 a1a2 b6b7 a2a1 b7b6 a1a2 b6b7"
|
||||
.split_whitespace()
|
||||
.map(Move::from_uci_algebraic)
|
||||
.map(|x| x.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected_bestmv = Move::from_uci_algebraic("a2a1").unwrap();
|
||||
|
||||
let mut cnt = 0;
|
||||
|
||||
for mv in &mvs {
|
||||
board.push_history();
|
||||
cnt += 1;
|
||||
let _ = mv.make(&mut board);
|
||||
}
|
||||
|
||||
eprintln!("board is: '{}'", board.to_fen());
|
||||
eprintln!("added {} history entries", cnt);
|
||||
|
||||
let (_tx, rx) = std::sync::mpsc::channel();
|
||||
let cache = TranspositionTable::new(1);
|
||||
|
||||
let mut engine_state = EngineState::new(
|
||||
SearchConfig {
|
||||
depth: 1,
|
||||
qdepth: 0,
|
||||
enable_trans_table: false,
|
||||
..Default::default()
|
||||
},
|
||||
rx,
|
||||
cache,
|
||||
search::TimeLimits::default(),
|
||||
);
|
||||
|
||||
let (line, eval) = best_line(&mut board, &mut engine_state);
|
||||
let best_mv = line.last().unwrap();
|
||||
|
||||
expected_bestmv.make(&mut board);
|
||||
eprintln!(
|
||||
"after expected mv, board repeated {} times",
|
||||
board.history.count(board.zobrist)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
*best_mv,
|
||||
expected_bestmv,
|
||||
"got {} (eval {:?}) instead of {}",
|
||||
best_mv.to_uci_algebraic(),
|
||||
eval,
|
||||
expected_bestmv.to_uci_algebraic()
|
||||
);
|
||||
assert!(EvalInt::from(eval) == 0);
|
||||
|
||||
// now ensure that it's completely one-sided without the repetition opportunity
|
||||
let mut board = Board::from_fen("qqqp4/pkpp4/8/8/8/8/8/K7 w - - 0 1").unwrap();
|
||||
|
||||
let (_tx, rx) = std::sync::mpsc::channel();
|
||||
let cache = TranspositionTable::new(1);
|
||||
let mut engine_state = EngineState::new(
|
||||
SearchConfig {
|
||||
depth: 1,
|
||||
qdepth: 0,
|
||||
enable_trans_table: false,
|
||||
..Default::default()
|
||||
},
|
||||
rx,
|
||||
cache,
|
||||
search::TimeLimits::default(),
|
||||
);
|
||||
|
||||
let (_line, eval) = best_line(&mut board, &mut engine_state);
|
||||
assert!(EvalInt::from(eval) < 0);
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ fn cmd_position_moves(mut tokens: std::str::SplitWhitespace<'_>, mut board: Boar
|
||||
"moves" => {
|
||||
for mv in tokens.by_ref() {
|
||||
let mv = Move::from_uci_algebraic(mv).unwrap();
|
||||
board.push_history();
|
||||
let _ = mv.make(&mut board);
|
||||
}
|
||||
}
|
||||
|
@ -122,8 +122,8 @@ impl Default for SearchConfig {
|
||||
fn default() -> Self {
|
||||
SearchConfig {
|
||||
alpha_beta_on: true,
|
||||
depth: 10,
|
||||
qdepth: 4,
|
||||
depth: 16,
|
||||
qdepth: 6,
|
||||
enable_trans_table: true,
|
||||
transposition_size: 24,
|
||||
}
|
||||
@ -208,12 +208,18 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
|
||||
}
|
||||
}
|
||||
|
||||
let is_repetition_draw = board.history.count(board.zobrist) >= 2;
|
||||
|
||||
// quiescence stand-pat score (only calculated if needed).
|
||||
// this is where static eval goes.
|
||||
let mut board_eval: Option<EvalInt> = None;
|
||||
|
||||
if mm.quiesce {
|
||||
board_eval = Some(board.eval() * EvalInt::from(board.turn.sign()));
|
||||
board_eval = Some(if is_repetition_draw {
|
||||
0
|
||||
} else {
|
||||
board.eval() * EvalInt::from(board.turn.sign())
|
||||
});
|
||||
}
|
||||
|
||||
if mm.depth == 0 {
|
||||
@ -341,6 +347,10 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
|
||||
}
|
||||
}
|
||||
|
||||
if is_repetition_draw {
|
||||
abs_best = SearchEval::Exact(0);
|
||||
}
|
||||
|
||||
if let Some(best_move) = best_move {
|
||||
best_continuation.push(best_move);
|
||||
if state.config.enable_trans_table {
|
||||
|
Loading…
Reference in New Issue
Block a user