Compare commits

...

8 Commits

Author SHA1 Message Date
b9819a52e6
tune: pst and transposition table 2024-11-23 22:39:39 -05:00
f415a9148c
fix: wrong logic on skepticism 2024-11-23 21:37:19 -05:00
c494230706 feat: skepticism
garbage mitigation for the horizon problem
2024-11-23 21:17:20 -05:00
e8cb125df9
tune: piece square tables
make it not be as crazy
2024-11-23 20:15:39 -05:00
3dcd27013d
feat: transposition table can now directly decide move
it's no longer just a suggestion
2024-11-23 19:13:02 -05:00
e79f19942e
perf: zobrist table no longer compares hash before overwrites 2024-11-23 18:51:49 -05:00
8895770da6
chore: fmt 2024-11-23 18:43:36 -05:00
af18600d15
refactor: move to main.rs file and add prelude 2024-11-23 18:42:36 -05:00
10 changed files with 97 additions and 71 deletions

View File

@ -4,8 +4,6 @@ version = "0.1.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
default-run = "engine"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]

View File

@ -1,15 +0,0 @@
//! Generates moves from the FEN in the argv.
use chess_inator::fen::FromFen;
use chess_inator::movegen::MoveGen;
use chess_inator::Board;
use std::env;
fn main() {
let fen = env::args().nth(1).unwrap();
let mut board = Board::from_fen(&fen).unwrap();
let mvs = board.gen_moves();
for mv in mvs.into_iter() {
println!("{mv:?}")
}
}

View File

@ -1,14 +0,0 @@
//! Runs perft at depth for a given FEN.
use chess_inator::fen::FromFen;
use chess_inator::movegen::perft;
use chess_inator::Board;
use std::env;
fn main() {
let depth = env::args().nth(1).unwrap().parse::<usize>().unwrap();
let fen = env::args().nth(2).unwrap();
let mut board = Board::from_fen(&fen).unwrap();
let res = perft(depth, &mut board);
println!("{res}")
}

View File

@ -193,7 +193,7 @@ pub const PST_MIDGAME: Pst = Pst([
-1, -1, 2, -1, 1, -1, 2, 1, // 4 -1, -1, 2, -1, 1, -1, 2, 1, // 4
2, 1, 1, 0, 0, 0, 0, 0, // 3 2, 1, 1, 0, 0, 0, 0, 0, // 3
0, 0, 0, 0, 0, 0, 0, 0, // 2 0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 10, 10, 5, 0, 0, // 1 -5, 0, 0, 10, 10, 5, 0, -5, // 1
// a b c d e f g h // a b c d e f g h
], 500), ], 500),
@ -217,7 +217,7 @@ pub const PST_MIDGAME: Pst = Pst([
-5, 1, 0, 0, 0, 0, 0, -5, // 6 -5, 1, 0, 0, 0, 0, 0, -5, // 6
-5, 2, 0, 10, 10, 0, 0, -5, // 5 -5, 2, 0, 10, 10, 0, 0, -5, // 5
-5, 0, 1, 10, 10, 0, 0, -5, // 4 -5, 0, 1, 10, 10, 0, 0, -5, // 4
-5, 2, 10, 0, 0, 10, 0, -5, // 3 -5, 2, 20, 0, 0, 20, 0, -5, // 3
-5, 1, 0, 0, 0, 0, 0, -5, // 2 -5, 1, 0, 0, 0, 0, 0, -5, // 2
-5, -5, -5, -5, -5, -5, -5, -5, // 1 -5, -5, -5, -5, -5, -5, -5, -5, // 1
// a b c d e f g h // a b c d e f g h
@ -231,18 +231,18 @@ pub const PST_MIDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 5 0, 0, 0, 0, 0, 0, 0, 0, // 5
0, 0, 0, 0, 0, 0, 0, 0, // 4 0, 0, 0, 0, 0, 0, 0, 0, // 4
0, 0, 0, 0, 0, 0, 0, 0, // 3 0, 0, 0, 0, 0, 0, 0, 0, // 3
0, 0, 0, 0, 0, 0, 0, 0, // 2 0, 0, 0, -5, -5, -5, 0, 0, // 2
0, 0, 10, 0, 0, 0, 20, 0, // 1 0, 0, 10, 0, 0, 0, 20, 0, // 1
// a b c d e f g h // a b c d e f g h
], 20_000), ], 20_000),
// queen // queen
make_pst([ make_pst([
0, 0, 0, 0, 0, 0, 0, 0, // 8 -20, -20, -20, -20, -20, -20, -20, -20, // 8
0, 0, 0, 0, 0, 0, 0, 0, // 7 -20, -20, -20, -20, -20, -20, -20, -20, // 7
0, 0, 0, 0, 0, 0, 0, 0, // 6 -20, -20, -20, -20, -20, -20, -20, -20, // 6
0, 0, 0, 0, 0, 0, 0, 0, // 5 -20, -20, -20, -20, -20, -20, -20, -20, // 5
0, 0, 0, 0, 0, 0, 0, 0, // 4 -20, -20, -20, -20, -20, -20, -20, -20, // 4
0, 0, 0, 0, 0, 0, 0, 0, // 3 0, 0, 0, 0, 0, 0, 0, 0, // 3
0, 0, 0, 0, 0, 0, 0, 0, // 2 0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, // 1 0, 0, 0, 0, 0, 0, 0, 0, // 1
@ -251,8 +251,8 @@ pub const PST_MIDGAME: Pst = Pst([
// pawn // pawn
make_pst([ make_pst([
10, 10, 10, 10, 10, 10, 10, 10, // 8 0, 0, 0, 0, 0, 0, 0, 0, // 8
9, 9, 9, 9, 9, 9, 9, 9, // 7 19, 19, 19, 19, 19, 19, 19, 19, // 7
8, 8, 8, 8, 8, 8, 8, 8, // 6 8, 8, 8, 8, 8, 8, 8, 8, // 6
7, 7, 7, 8, 8, 7, 7, 7, // 5 7, 7, 7, 8, 8, 7, 7, 7, // 5
6, 6, 6, 6, 6, 6, 6, 6, // 4 6, 6, 6, 6, 6, 6, 6, 6, // 4

View File

@ -20,8 +20,10 @@ use crate::{
use std::ops::Index; use std::ops::Index;
use std::ops::IndexMut; use std::ops::IndexMut;
const PIECE_KEYS: [[[u64; N_SQUARES]; N_PIECES]; N_COLORS] = const PIECE_KEYS: [[[u64; N_SQUARES]; N_PIECES]; N_COLORS] = [
[Pcg64Random::new(11).random_arr_2d_64(), Pcg64Random::new(22).random_arr_2d_64()]; Pcg64Random::new(11).random_arr_2d_64(),
Pcg64Random::new(22).random_arr_2d_64(),
];
// 4 bits in castle perms -> 16 keys // 4 bits in castle perms -> 16 keys
const CASTLE_KEYS: [u64; 16] = Pcg64Random::new(33).random_arr_64(); const CASTLE_KEYS: [u64; 16] = Pcg64Random::new(33).random_arr_64();
@ -100,7 +102,10 @@ pub struct ZobristTable<T> {
impl<T: Copy> ZobristTable<T> { impl<T: Copy> ZobristTable<T> {
/// Create a table with 2^n entries. /// Create a table with 2^n entries.
pub fn new(size: usize) -> Self { pub fn new(size: usize) -> Self {
assert!(size <= 27, "Attempted to make 2^{size} entry table; aborting to avoid excessive memory usage."); assert!(
size <= 27,
"Attempted to make 2^{size} entry table; aborting to avoid excessive memory usage."
);
ZobristTable { ZobristTable {
data: vec![(Zobrist { hash: 0 }, None); 1 << size], data: vec![(Zobrist { hash: 0 }, None); 1 << size],
size, size,
@ -109,16 +114,15 @@ impl<T: Copy> ZobristTable<T> {
} }
impl<T> IndexMut<Zobrist> for ZobristTable<T> { impl<T> IndexMut<Zobrist> for ZobristTable<T> {
/// Overwrite a table entry.
///
/// If you `mut`ably index, it will automatically wipe an existing entry,
/// regardless of it was a cache hit or miss.
fn index_mut(&mut self, zobrist: Zobrist) -> &mut Self::Output { fn index_mut(&mut self, zobrist: Zobrist) -> &mut Self::Output {
let idx = zobrist.truncate_hash(self.size); let idx = zobrist.truncate_hash(self.size);
if self.data[idx].0 == zobrist { self.data[idx].0 = zobrist;
&mut self.data[idx].1 self.data[idx].1 = None;
} else { &mut self.data[idx].1
// miss, overwrite
self.data[idx].0 = zobrist;
self.data[idx].1 = None;
&mut self.data[idx].1
}
} }
} }
@ -212,7 +216,10 @@ mod tests {
} }
pos.half_moves = pos_orig.half_moves; pos.half_moves = pos_orig.half_moves;
pos.full_moves = pos_orig.full_moves; pos.full_moves = pos_orig.full_moves;
assert_eq!(pos, pos_orig, "test case is incorrect, position should loop back to the original"); assert_eq!(
pos, pos_orig,
"test case is incorrect, position should loop back to the original"
);
assert_eq!(pos.zobrist, pos_orig.zobrist); assert_eq!(pos.zobrist, pos_orig.zobrist);
} }
} }
@ -222,7 +229,9 @@ mod tests {
let mut table = ZobristTable::<usize>::new(4); let mut table = ZobristTable::<usize>::new(4);
macro_rules! z { macro_rules! z {
($i: expr) => { Zobrist { hash: $i } } ($i: expr) => {
Zobrist { hash: $i }
};
} }
let big_number = 1 << 62; let big_number = 1 << 62;

View File

@ -24,13 +24,15 @@ pub mod movegen;
pub mod random; pub mod random;
pub mod search; pub mod search;
pub mod prelude;
use crate::fen::{FromFen, ToFen, START_POSITION}; use crate::fen::{FromFen, ToFen, START_POSITION};
use crate::hash::Zobrist; use crate::hash::Zobrist;
use eval::eval_score::EvalScores; use eval::eval_score::EvalScores;
const BOARD_WIDTH: usize = 8; pub const BOARD_WIDTH: usize = 8;
const BOARD_HEIGHT: usize = 8; pub const BOARD_HEIGHT: usize = 8;
const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT; pub const N_SQUARES: usize = BOARD_WIDTH * BOARD_HEIGHT;
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
pub enum Color { pub enum Color {
@ -38,7 +40,7 @@ pub enum Color {
White = 0, White = 0,
Black = 1, Black = 1,
} }
const N_COLORS: usize = 2; pub const N_COLORS: usize = 2;
impl Color { impl Color {
/// Return opposite color (does not assign). /// Return opposite color (does not assign).
@ -74,7 +76,7 @@ enum Piece {
Queen, Queen,
Pawn, Pawn,
} }
const N_PIECES: usize = 6; pub const N_PIECES: usize = 6;
pub struct PieceErr; pub struct PieceErr;

View File

@ -12,11 +12,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
//! Main UCI engine binary. //! Main UCI engine binary.
use chess_inator::eval::eval_metrics; use chess_inator::prelude::*;
use chess_inator::fen::FromFen;
use chess_inator::movegen::{FromUCIAlgebraic, Move, ToUCIAlgebraic};
use chess_inator::search::{best_line, InterfaceMsg, SearchEval, TranspositionTable};
use chess_inator::{Board, Color};
use std::cmp::min; use std::cmp::min;
use std::io; use std::io;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;

View File

@ -770,7 +770,11 @@ impl MoveGenInternal for Board {
} }
} }
fn perft_internal(depth: usize, pos: &mut Board, cache: &mut ZobristTable<(usize, usize)>) -> usize { fn perft_internal(
depth: usize,
pos: &mut Board,
cache: &mut ZobristTable<(usize, usize)>,
) -> usize {
if let Some((ans, cache_depth)) = cache[pos.zobrist] { if let Some((ans, cache_depth)) = cache[pos.zobrist] {
if depth == cache_depth { if depth == cache_depth {
return ans; return ans;

20
src/prelude.rs Normal file
View File

@ -0,0 +1,20 @@
/*
This file is part of chess_inator.
chess_inator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
chess_inator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with chess_inator. If not, see https://www.gnu.org/licenses/.
Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
*/
//! Prelude that you can import entirely to use the library conveniently.
pub use crate::eval::{eval_metrics, EvalMetrics};
pub use crate::fen::{FromFen, ToFen};
pub use crate::movegen::{FromUCIAlgebraic, Move, MoveGen, ToUCIAlgebraic};
pub use crate::search::{best_line, best_move, InterfaceMsg, SearchEval, TranspositionTable};
pub use crate::{Board, Color, BOARD_HEIGHT, BOARD_WIDTH, N_COLORS, N_PIECES, N_SQUARES};

View File

@ -105,6 +105,7 @@ impl Default for SearchConfig {
fn default() -> Self { fn default() -> Self {
SearchConfig { SearchConfig {
alpha_beta_on: true, alpha_beta_on: true,
// try to make this even to be more conservative and avoid horizon problem
depth: 10, depth: 10,
} }
} }
@ -175,9 +176,15 @@ fn minmax(
.map(|mv| (move_priority(board, &mv), mv)) .map(|mv| (move_priority(board, &mv), mv))
.collect(); .collect();
// remember the prior best move // get transposition table entry
if let Some(cache) = cache { if let Some(cache) = cache {
if let Some(entry) = &cache[board.zobrist] { if let Some(entry) = &cache[board.zobrist] {
// the entry has a deeper knowledge than we do, so follow its best move exactly instead of
// just prioritizing what it thinks is best
if entry.depth >= depth {
// we don't save PV line in transposition table, so no information on that
return (vec![entry.best_move], entry.eval)
}
mvs.push((EVAL_BEST, entry.best_move)); mvs.push((EVAL_BEST, entry.best_move));
} }
} }
@ -224,7 +231,11 @@ fn minmax(
if let Some(best_move) = best_move { if let Some(best_move) = best_move {
best_continuation.push(best_move); best_continuation.push(best_move);
if let Some(cache) = cache { if let Some(cache) = cache {
cache[board.zobrist] = Some(TranspositionEntry { best_move }); cache[board.zobrist] = Some(TranspositionEntry {
best_move,
eval: abs_best,
depth,
});
} }
} }
@ -240,8 +251,12 @@ type InterfaceRx = mpsc::Receiver<InterfaceMsg>;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct TranspositionEntry { pub struct TranspositionEntry {
// best move found last time /// best move found last time
best_move: Move, best_move: Move,
/// last time's eval
eval: SearchEval,
/// depth of this entry
depth: usize,
} }
pub type TranspositionTable = ZobristTable<TranspositionEntry>; pub type TranspositionTable = ZobristTable<TranspositionEntry>;
@ -254,14 +269,24 @@ fn iter_deep(
interface: Option<InterfaceRx>, interface: Option<InterfaceRx>,
cache: &mut TranspositionTableOpt, cache: &mut TranspositionTableOpt,
) -> (Vec<Move>, SearchEval) { ) -> (Vec<Move>, SearchEval) {
for depth in 1..=config.depth { let (mut prev_line, mut prev_eval) = minmax(board, config, 1, None, None, cache);
for depth in 2..=config.depth {
let (line, eval) = minmax(board, config, depth, None, None, cache); let (line, eval) = minmax(board, config, depth, None, None, cache);
if let Some(ref rx) = interface { if let Some(ref rx) = interface {
// don't interrupt a depth 1 search so that there's at least a move to be played // don't interrupt a depth 1 search so that there's at least a move to be played
if depth != 1 { if depth != 1 {
match rx.try_recv() { match rx.try_recv() {
Ok(msg) => match msg { Ok(msg) => match msg {
InterfaceMsg::Stop => return (line, eval), InterfaceMsg::Stop => {
if depth & 1 == 1 && (EvalInt::from(eval) - EvalInt::from(prev_eval) > 300) {
// be skeptical if we move last and we suddenly earn a lot of
// centipawns. this may be a sign of horizon problem
return (prev_line, prev_eval)
} else {
return (line, eval)
}
},
}, },
Err(e) => match e { Err(e) => match e {
mpsc::TryRecvError::Empty => {} mpsc::TryRecvError::Empty => {}
@ -272,8 +297,9 @@ fn iter_deep(
} else if depth == config.depth - 1 { } else if depth == config.depth - 1 {
return (line, eval); return (line, eval);
} }
(prev_line, prev_eval) = (line, eval);
} }
panic!("iterative deepening did not search at all") (prev_line, prev_eval)
} }
/// Find the best line (in reverse order) and its evaluation. /// Find the best line (in reverse order) and its evaluation.