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

View File

@ -20,8 +20,10 @@ use crate::{
use std::ops::Index;
use std::ops::IndexMut;
const PIECE_KEYS: [[[u64; N_SQUARES]; N_PIECES]; N_COLORS] =
[Pcg64Random::new(11).random_arr_2d_64(), Pcg64Random::new(22).random_arr_2d_64()];
const PIECE_KEYS: [[[u64; N_SQUARES]; N_PIECES]; N_COLORS] = [
Pcg64Random::new(11).random_arr_2d_64(),
Pcg64Random::new(22).random_arr_2d_64(),
];
// 4 bits in castle perms -> 16 keys
const CASTLE_KEYS: [u64; 16] = Pcg64Random::new(33).random_arr_64();
@ -100,7 +102,10 @@ pub struct ZobristTable<T> {
impl<T: Copy> ZobristTable<T> {
/// Create a table with 2^n entries.
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 {
data: vec![(Zobrist { hash: 0 }, None); 1 << size],
size,
@ -109,16 +114,15 @@ impl<T: Copy> 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 {
let idx = zobrist.truncate_hash(self.size);
if self.data[idx].0 == zobrist {
&mut self.data[idx].1
} else {
// miss, overwrite
self.data[idx].0 = zobrist;
self.data[idx].1 = None;
&mut self.data[idx].1
}
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.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);
}
}
@ -222,7 +229,9 @@ mod tests {
let mut table = ZobristTable::<usize>::new(4);
macro_rules! z {
($i: expr) => { Zobrist { hash: $i } }
($i: expr) => {
Zobrist { hash: $i }
};
}
let big_number = 1 << 62;

View File

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

View File

@ -12,11 +12,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
//! Main UCI engine binary.
use chess_inator::eval::eval_metrics;
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 chess_inator::prelude::*;
use std::cmp::min;
use std::io;
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 depth == cache_depth {
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 {
SearchConfig {
alpha_beta_on: true,
// try to make this even to be more conservative and avoid horizon problem
depth: 10,
}
}
@ -175,9 +176,15 @@ fn minmax(
.map(|mv| (move_priority(board, &mv), mv))
.collect();
// remember the prior best move
// get transposition table entry
if let Some(cache) = cache {
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));
}
}
@ -224,7 +231,11 @@ fn minmax(
if let Some(best_move) = best_move {
best_continuation.push(best_move);
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)]
pub struct TranspositionEntry {
// best move found last time
/// best move found last time
best_move: Move,
/// last time's eval
eval: SearchEval,
/// depth of this entry
depth: usize,
}
pub type TranspositionTable = ZobristTable<TranspositionEntry>;
@ -254,14 +269,24 @@ fn iter_deep(
interface: Option<InterfaceRx>,
cache: &mut TranspositionTableOpt,
) -> (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);
if let Some(ref rx) = interface {
// don't interrupt a depth 1 search so that there's at least a move to be played
if depth != 1 {
match rx.try_recv() {
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 {
mpsc::TryRecvError::Empty => {}
@ -272,8 +297,9 @@ fn iter_deep(
} else if depth == config.depth - 1 {
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.