Compare commits
No commits in common. "87501b5c94bb22add7e054531887a1457b117412" and "caa3bc454cd5a7904f3de9fe34cc417e7be84eff" have entirely different histories.
87501b5c94
...
caa3bc454c
@ -165,8 +165,6 @@ pub struct MsgBestmove {
|
|||||||
pub pv: Vec<Move>,
|
pub pv: Vec<Move>,
|
||||||
/// Evaluation of the position
|
/// Evaluation of the position
|
||||||
pub eval: SearchEval,
|
pub eval: SearchEval,
|
||||||
/// Extra information (displayed as `info string`).
|
|
||||||
pub info: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interface messages that may be received by main's channel.
|
/// Interface messages that may be received by main's channel.
|
||||||
|
@ -11,7 +11,7 @@ You should have received a copy of the GNU General Public License along with che
|
|||||||
Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//! Static position evaluation (hand-crafted eval).
|
//! Position evaluation.
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use core::cmp::{max, min};
|
use core::cmp::{max, min};
|
||||||
|
19
src/lib.rs
19
src/lib.rs
@ -19,7 +19,6 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
pub mod coordination;
|
pub mod coordination;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod nnue;
|
|
||||||
pub mod fen;
|
pub mod fen;
|
||||||
mod hash;
|
mod hash;
|
||||||
pub mod movegen;
|
pub mod movegen;
|
||||||
@ -517,8 +516,9 @@ mod ringptr_tests {
|
|||||||
|
|
||||||
/// Ring-buffer of previously seen hashes, used to avoid draw by repetition.
|
/// Ring-buffer of previously seen hashes, used to avoid draw by repetition.
|
||||||
///
|
///
|
||||||
/// Only stores at most `HISTORY_SIZE` plies.
|
/// Only stores at most `HISTORY_SIZE` plies, since most cases of repetition happen recently.
|
||||||
#[derive(Clone, Copy, Debug)]
|
/// Technically, it should be 100 plies because of the 50-move rule.
|
||||||
|
#[derive(Default, Clone, Copy, Debug)]
|
||||||
struct BoardHistory {
|
struct BoardHistory {
|
||||||
hashes: [Zobrist; HISTORY_SIZE],
|
hashes: [Zobrist; HISTORY_SIZE],
|
||||||
/// Index of the start of the history in the buffer
|
/// Index of the start of the history in the buffer
|
||||||
@ -527,17 +527,6 @@ struct BoardHistory {
|
|||||||
ptr_end: RingPtr<HISTORY_SIZE>,
|
ptr_end: RingPtr<HISTORY_SIZE>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BoardHistory {
|
|
||||||
fn default() -> Self {
|
|
||||||
BoardHistory {
|
|
||||||
// rust can't derive this
|
|
||||||
hashes: [Zobrist::default(); HISTORY_SIZE],
|
|
||||||
ptr_start: Default::default(),
|
|
||||||
ptr_end: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for BoardHistory {
|
impl PartialEq for BoardHistory {
|
||||||
/// Always equal, since comparing two boards with different histories shouldn't matter.
|
/// Always equal, since comparing two boards with different histories shouldn't matter.
|
||||||
fn eq(&self, _other: &Self) -> bool {
|
fn eq(&self, _other: &Self) -> bool {
|
||||||
@ -550,7 +539,7 @@ impl Eq for BoardHistory {}
|
|||||||
/// Size in plies of the board history.
|
/// Size in plies of the board history.
|
||||||
///
|
///
|
||||||
/// Actual capacity is one less than this.
|
/// Actual capacity is one less than this.
|
||||||
const HISTORY_SIZE: usize = 100;
|
const HISTORY_SIZE: usize = 15;
|
||||||
|
|
||||||
impl BoardHistory {
|
impl BoardHistory {
|
||||||
/// Counts occurences of this hash in the history.
|
/// Counts occurences of this hash in the history.
|
||||||
|
111
src/main.rs
111
src/main.rs
@ -51,9 +51,8 @@ macro_rules! ignore {
|
|||||||
/// UCI engine metadata query.
|
/// UCI engine metadata query.
|
||||||
fn cmd_uci() -> String {
|
fn cmd_uci() -> String {
|
||||||
let str = "id name chess_inator\n\
|
let str = "id name chess_inator\n\
|
||||||
id author dogeystamp\n\
|
id author dogeystamp\n\
|
||||||
option name NNUETrainInfo type check default false\n\
|
uciok";
|
||||||
uciok";
|
|
||||||
str.into()
|
str.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,14 +111,15 @@ fn cmd_position(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState
|
|||||||
|
|
||||||
/// Play the game.
|
/// Play the game.
|
||||||
fn cmd_go(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
fn cmd_go(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
||||||
let mut wtime: Option<u64> = None;
|
let mut wtime = 0;
|
||||||
let mut btime: Option<u64> = None;
|
let mut btime = 0;
|
||||||
let mut movetime: Option<u64> = None;
|
|
||||||
|
|
||||||
macro_rules! set_time {
|
macro_rules! set_time {
|
||||||
($var: ident) => {
|
($color: expr, $var: ident) => {
|
||||||
if let Some(time) = tokens.next() {
|
if let Some(time) = tokens.next() {
|
||||||
$var = time.parse::<u64>().ok();
|
if let Ok(time) = time.parse::<u64>() {
|
||||||
|
$var = time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -127,40 +127,35 @@ fn cmd_go(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
|||||||
while let Some(token) = tokens.next() {
|
while let Some(token) = tokens.next() {
|
||||||
match token {
|
match token {
|
||||||
"wtime" => {
|
"wtime" => {
|
||||||
set_time!(wtime)
|
set_time!(Color::White, wtime)
|
||||||
}
|
}
|
||||||
"btime" => {
|
"btime" => {
|
||||||
set_time!(btime)
|
set_time!(Color::Black, btime)
|
||||||
}
|
|
||||||
"movetime" => {
|
|
||||||
set_time!(movetime)
|
|
||||||
}
|
}
|
||||||
_ => ignore!(),
|
_ => ignore!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (ourtime_ms, theirtime_ms) = if state.board.get_turn() == Color::White {
|
let (mut ourtime_ms, theirtime_ms) = if state.board.get_turn() == Color::White {
|
||||||
(wtime, btime)
|
(wtime, btime)
|
||||||
} else {
|
} else {
|
||||||
(btime, wtime)
|
(btime, wtime)
|
||||||
};
|
};
|
||||||
|
|
||||||
let time_lims = if let Some(movetime) = movetime {
|
if ourtime_ms == 0 {
|
||||||
TimeLimits::from_movetime(movetime)
|
ourtime_ms = 300_000
|
||||||
} else {
|
}
|
||||||
TimeLimits::from_ourtime_theirtime(
|
|
||||||
ourtime_ms.unwrap_or(300_000),
|
|
||||||
theirtime_ms.unwrap_or(300_000),
|
|
||||||
eval_metrics(&state.board),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
state
|
state
|
||||||
.tx_engine
|
.tx_engine
|
||||||
.send(MsgToEngine::Go(Box::new(GoMessage {
|
.send(MsgToEngine::Go(Box::new(GoMessage {
|
||||||
board: state.board,
|
board: state.board,
|
||||||
config: state.config,
|
config: state.config,
|
||||||
time_lims,
|
time_lims: TimeLimits::from_ourtime_theirtime(
|
||||||
|
ourtime_ms,
|
||||||
|
theirtime_ms,
|
||||||
|
eval_metrics(&state.board),
|
||||||
|
),
|
||||||
})))
|
})))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@ -207,50 +202,6 @@ fn cmd_eval(mut _tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
|||||||
println!("- total: {}", res.total_eval);
|
println!("- total: {}", res.total_eval);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_true_false(s: &str) -> Option<bool> {
|
|
||||||
match s {
|
|
||||||
"true" => Some(true),
|
|
||||||
"false" => Some(false),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set engine options via UCI.
|
|
||||||
fn cmd_setoption(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
|
||||||
while let Some(token) = tokens.next() {
|
|
||||||
fn get_val(mut tokens: std::str::SplitWhitespace<'_>) -> Option<String> {
|
|
||||||
if let Some("value") = tokens.next() {
|
|
||||||
if let Some(value) = tokens.next() {
|
|
||||||
return Some(value.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
match token {
|
|
||||||
"name" => {
|
|
||||||
if let Some(name) = tokens.next() {
|
|
||||||
match name {
|
|
||||||
"NNUETrainInfo" => {
|
|
||||||
if let Some(value) = get_val(tokens) {
|
|
||||||
if let Some(value) = match_true_false(&value) {
|
|
||||||
state.config.nnue_train_info = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
println!("info string Unknown option: {}", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => ignore!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Root UCI parser.
|
/// Root UCI parser.
|
||||||
fn cmd_root(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
fn cmd_root(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
||||||
while let Some(token) = tokens.next() {
|
while let Some(token) = tokens.next() {
|
||||||
@ -286,9 +237,6 @@ fn cmd_root(mut tokens: std::str::SplitWhitespace<'_>, state: &mut MainState) {
|
|||||||
state.tx_engine.send(MsgToEngine::Stop).unwrap();
|
state.tx_engine.send(MsgToEngine::Stop).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"setoption" => {
|
|
||||||
cmd_setoption(tokens, state);
|
|
||||||
}
|
|
||||||
// non-standard command.
|
// non-standard command.
|
||||||
"eval" => {
|
"eval" => {
|
||||||
cmd_eval(tokens, state);
|
cmd_eval(tokens, state);
|
||||||
@ -321,10 +269,6 @@ fn outp_bestmove(bestmove: MsgBestmove) {
|
|||||||
panic!("info string ERROR: stopped search")
|
panic!("info string ERROR: stopped search")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for line in bestmove.info {
|
|
||||||
println!("info string {line}");
|
|
||||||
}
|
|
||||||
|
|
||||||
match chosen {
|
match chosen {
|
||||||
Some(mv) => println!("bestmove {}", mv.to_uci_algebraic()),
|
Some(mv) => println!("bestmove {}", mv.to_uci_algebraic()),
|
||||||
None => println!("bestmove 0000"),
|
None => println!("bestmove 0000"),
|
||||||
@ -366,23 +310,8 @@ fn task_engine(tx_main: Sender<MsgToMain>, rx_engine: Receiver<MsgToEngine>) {
|
|||||||
state.config = msg_box.config;
|
state.config = msg_box.config;
|
||||||
state.time_lims = msg_box.time_lims;
|
state.time_lims = msg_box.time_lims;
|
||||||
let (pv, eval) = best_line(&mut board, &mut state);
|
let (pv, eval) = best_line(&mut board, &mut state);
|
||||||
|
|
||||||
let mut info: Vec<String> = Vec::new();
|
|
||||||
if state.config.nnue_train_info {
|
|
||||||
let is_quiet = chess_inator::search::is_quiescent_position(&board, eval);
|
|
||||||
let is_quiet = if is_quiet {"quiet"} else {"non-quiet"};
|
|
||||||
|
|
||||||
let board_tensor = chess_inator::nnue::InputTensor::from_board(&board);
|
|
||||||
|
|
||||||
info.push(format!("NNUETrainInfo {} {}", is_quiet, {board_tensor}))
|
|
||||||
}
|
|
||||||
|
|
||||||
tx_main
|
tx_main
|
||||||
.send(MsgToMain::Bestmove(MsgBestmove {
|
.send(MsgToMain::Bestmove(MsgBestmove { pv, eval }))
|
||||||
pv,
|
|
||||||
eval,
|
|
||||||
info,
|
|
||||||
}))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
MsgToEngine::Stop => {}
|
MsgToEngine::Stop => {}
|
||||||
|
92
src/nnue.rs
92
src/nnue.rs
@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
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>
|
|
||||||
*/
|
|
||||||
|
|
||||||
//! Static position evaluation (neural network based eval).
|
|
||||||
//!
|
|
||||||
//! # Neural net architecture
|
|
||||||
//!
|
|
||||||
//! The NNUE has the following layers:
|
|
||||||
//!
|
|
||||||
//! * Input (board features)
|
|
||||||
//! * Hidden layer / accumulator (N neurons)
|
|
||||||
//! * Output layer (Single neuron)
|
|
||||||
//!
|
|
||||||
//! The input layer is a multi-hot binary tensor that represents the board. It is a product of
|
|
||||||
//! color (2), piece type (6) and piece position (64), giving a total of 768 elements representing
|
|
||||||
//! for example "is there a _white_, _pawn_ at _e4_?". This information is not enough to represent
|
|
||||||
//! the board, but is enough for static evaluation purposes. Our NNUE is only expected to run on
|
|
||||||
//! quiescent positions, and our traditional minmax algorithm will take care of any exchanges, en
|
|
||||||
//! passant, and other rules that can be mechanically applied.
|
|
||||||
//!
|
|
||||||
//! In the engine, the input layer is imaginary. Because of the nature of NNUE (efficiently
|
|
||||||
//! updatable neural network), we only store the hidden layer's state, and whenever we want to flip
|
|
||||||
//! a bit in the input layer, we directly add/subtract the corresponding weight from the hidden
|
|
||||||
//! layer.
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
/// Size of the input feature tensor.
|
|
||||||
pub const INP_TENSOR_SIZE: usize = N_COLORS * N_PIECES * N_SQUARES;
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct InputTensor([bool; INP_TENSOR_SIZE]);
|
|
||||||
|
|
||||||
/// Input tensor for the NNUE.
|
|
||||||
///
|
|
||||||
/// Note that this tensor does not exist at runtime, only during training.
|
|
||||||
impl InputTensor {
|
|
||||||
/// Calculate index within the input tensor of a piece/color/square combination.
|
|
||||||
pub fn idx(pc: ColPiece, sq: Square) -> usize {
|
|
||||||
let col = pc.col as usize;
|
|
||||||
let pc = pc.pc as usize;
|
|
||||||
let sq = sq.0 as usize;
|
|
||||||
|
|
||||||
let ret = col * (N_PIECES * N_SQUARES) + pc * (N_SQUARES) + sq;
|
|
||||||
debug_assert!((0..INP_TENSOR_SIZE).contains(&ret));
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create the tensor from a board.
|
|
||||||
pub fn from_board(board: &Board) -> Self {
|
|
||||||
let mut tensor = [false; INP_TENSOR_SIZE];
|
|
||||||
for sq in Board::squares() {
|
|
||||||
if let Some(pc) = board.get_piece(sq) {
|
|
||||||
let idx = Self::idx(pc, sq);
|
|
||||||
tensor[idx] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InputTensor(tensor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for InputTensor {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let str = String::from_iter(self.0.map(|x| if x { '1' } else { '0' }));
|
|
||||||
write!(f, "{}", str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn test_to_binary_tensor() {
|
|
||||||
// more of a sanity check than a test
|
|
||||||
let board = Board::from_fen("8/8/8/8/8/8/8/1b6 w - - 0 1").unwrap();
|
|
||||||
let tensor = InputTensor::from_board(&board);
|
|
||||||
let mut expected = [false; INP_TENSOR_SIZE];
|
|
||||||
expected[INP_TENSOR_SIZE / N_COLORS + 1 + N_SQUARES] = true;
|
|
||||||
assert_eq!(tensor.0, expected);
|
|
||||||
}
|
|
||||||
}
|
|
@ -128,8 +128,6 @@ pub struct SearchConfig {
|
|||||||
pub enable_trans_table: bool,
|
pub enable_trans_table: bool,
|
||||||
/// Transposition table size (2^n where this is n)
|
/// Transposition table size (2^n where this is n)
|
||||||
pub transposition_size: usize,
|
pub transposition_size: usize,
|
||||||
/// Print machine-readable information about the position during NNUE training data generation.
|
|
||||||
pub nnue_train_info: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SearchConfig {
|
impl Default for SearchConfig {
|
||||||
@ -141,7 +139,6 @@ impl Default for SearchConfig {
|
|||||||
contempt: 0,
|
contempt: 0,
|
||||||
enable_trans_table: true,
|
enable_trans_table: true,
|
||||||
transposition_size: 24,
|
transposition_size: 24,
|
||||||
nnue_train_info: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,7 +450,11 @@ impl TimeLimits {
|
|||||||
/// Make time limits based on wtime, btime (but color-independent).
|
/// Make time limits based on wtime, btime (but color-independent).
|
||||||
///
|
///
|
||||||
/// Also takes in eval metrics, for instance to avoid wasting too much time in the opening.
|
/// Also takes in eval metrics, for instance to avoid wasting too much time in the opening.
|
||||||
pub fn from_ourtime_theirtime(ourtime_ms: u64, _theirtime_ms: u64, eval: EvalMetrics) -> Self {
|
pub fn from_ourtime_theirtime(
|
||||||
|
ourtime_ms: u64,
|
||||||
|
_theirtime_ms: u64,
|
||||||
|
eval: EvalMetrics,
|
||||||
|
) -> TimeLimits {
|
||||||
// hard timeout (max)
|
// hard timeout (max)
|
||||||
let mut hard_ms = 100_000;
|
let mut hard_ms = 100_000;
|
||||||
// soft timeout (default max)
|
// soft timeout (default max)
|
||||||
@ -487,16 +488,6 @@ impl TimeLimits {
|
|||||||
soft: Some(soft_limit),
|
soft: Some(soft_limit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make time limit based on an exact hard limit.
|
|
||||||
pub fn from_movetime(movetime_ms: u64) -> Self {
|
|
||||||
let hard_limit = Instant::now() + Duration::from_millis(movetime_ms);
|
|
||||||
|
|
||||||
TimeLimits {
|
|
||||||
hard: Some(hard_limit),
|
|
||||||
soft: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper type to avoid retyping the same arguments into every function prototype.
|
/// Helper type to avoid retyping the same arguments into every function prototype.
|
||||||
@ -549,27 +540,3 @@ pub fn best_move(board: &mut Board, engine_state: &mut EngineState) -> Option<Mo
|
|||||||
let (line, _eval) = best_line(board, engine_state);
|
let (line, _eval) = best_line(board, engine_state);
|
||||||
line.last().copied()
|
line.last().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utility for NNUE training set generation to determine if a position is quiet or not.
|
|
||||||
///
|
|
||||||
/// Our definition of "quiet" is that there are no checks, and the static and quiescence search
|
|
||||||
/// evaluations are similar. (See https://arxiv.org/html/2412.17948v1.)
|
|
||||||
///
|
|
||||||
/// It is the caller's responsibility to get the search evaluation and pass it to this function.
|
|
||||||
pub fn is_quiescent_position(board: &Board, eval: SearchEval) -> bool {
|
|
||||||
// max centipawn value difference to call "similar"
|
|
||||||
const THRESHOLD: EvalInt = 170;
|
|
||||||
|
|
||||||
if board.is_check(board.turn) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(eval, SearchEval::Checkmate(_)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// white perspective
|
|
||||||
let abs_eval = EvalInt::from(eval) * EvalInt::from(board.turn.sign());
|
|
||||||
|
|
||||||
(board.eval() - EvalInt::from(abs_eval)).abs() <= THRESHOLD.abs()
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user