From b9f363dcf62e0b96712970dfab027964526cd071 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 19 Apr 2025 23:38:26 +0200 Subject: [PATCH 1/9] spring cleaning --- Cargo.toml | 2 +- README.md | 5 ++-- src/benches/eval.rs | 2 +- src/benches/eval_grind.rs | 2 +- src/benches/move_gen.rs | 4 +-- src/benches/move_gen_grind.rs | 4 +-- src/engine/evaluation/mod.rs | 4 +-- src/engine/evaluation/tests/mod.rs | 28 ++++--------------- src/engine/move_generation/mod.rs | 7 ++--- .../moveordering.rs | 2 +- .../mv_heuristics.rs | 0 .../tests/moveordering.rs | 6 ++-- src/engine/search/mod.rs | 2 -- src/engine/search/tests/negatest.rs | 2 +- src/sandy/player/parse_move.rs | 2 +- 15 files changed, 25 insertions(+), 47 deletions(-) rename src/engine/{search => move_generation}/moveordering.rs (97%) rename src/engine/{search => move_generation}/mv_heuristics.rs (100%) rename src/engine/{search => move_generation}/tests/moveordering.rs (96%) diff --git a/Cargo.toml b/Cargo.toml index f334fde6..82e78707 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chesseng" -version = "0.6.2" +version = "0.6.3" edition = "2024" authors = ["Andreas Tsatsanis"] diff --git a/README.md b/README.md index 6a29484c..78736133 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ the evaluation function computed at every leaf node resides in [`./src/engine/ev everything else in the module is a helper to the main `evaluate()` function #### Move Generation -deceptive name since I use [`jordanbray/chess`](/jordanbray/chess) for the actual *generation* of moves (as well as for board & bitboard representations). this module is responsible for *move ordering*, ie giving the moves to the search function in order from best to worst, based on a heuristic guess. +deceptive name since I use [`jordanbray/chess`](https://github.com/jordanbray/chess) for the actual *generation* of moves (as well as for board & bitboard representations). this module is responsible for *move ordering*, ie giving the moves to the search function in order from best to worst, based on a heuristic guess. since alpha/beta pruning relies on this, it has a **huge** impact on engine performance. @@ -84,9 +84,10 @@ everything under [`./src/sandy/`](src/sandy/) is part of the frontend (almost) - [`./src/sandy/player/`](src/sandy/player/) is for playing against the engine CLI using a TUI - [`./src/sandy/uci/mod.rs`](src/sandy/uci/mod.rs) handles the UCI part of the CLI: converting commands to engine internal instructions - [`./src/sandy/uci/time_control.rs`](src/sandy/uci/time_control.rs) passes UCI time controls (eg `go btime 1000 wtime 1000`) to the engine, while also heuristically calculating how much time the engine should think for -- [`./src/sandy/uci/search_control.rs`](src/sandy/uci/search_control.rs) does the same for search controls (eg `go depth 4`) +- [`./src/sandy/uci/search_controls.rs`](src/sandy/uci/search_controls.rs) does the same for search controls (eg `go depth 4`) ## Changelog +- `v0.6.3` TBD, TODO - `v0.6.2` inline move ordering: switch from an allocated `Vec` to an iterator that only generates moves as needed, performing all move ordering operations on the construction of the iterator. - `v0.6.1` variable search depth: when a node has <= 3 children, increase search depth by 1, just for this case. this massively helps lookahead in positions with a lot of checks - lost versions: i did not actually keep a changelog until `v0.6.1`. i do not remember the details here diff --git a/src/benches/eval.rs b/src/benches/eval.rs index c2e323d8..2327224d 100644 --- a/src/benches/eval.rs +++ b/src/benches/eval.rs @@ -4,8 +4,8 @@ use criterion::Criterion; use criterion::black_box; use criterion::criterion_group; use criterion::criterion_main; +use sandy_engine::move_generation::moveordering::ordered_moves; use sandy_engine::opts::Opts; -use sandy_engine::search::moveordering::ordered_moves; use sandy_engine::util::bench_positions; /// Benchmark the evaluation function diff --git a/src/benches/eval_grind.rs b/src/benches/eval_grind.rs index 2b0692bc..8bc4af0f 100644 --- a/src/benches/eval_grind.rs +++ b/src/benches/eval_grind.rs @@ -1,7 +1,7 @@ //! This file contains valgrind benchmarks for the evaluation function. use chess::Board; use iai::black_box; -use sandy_engine::search::moveordering::unordered_moves; +use sandy_engine::move_generation::moveordering::unordered_moves; /// how many instructions does it take to set up a board fn board_setup() { diff --git a/src/benches/move_gen.rs b/src/benches/move_gen.rs index 1a669b40..a6911526 100644 --- a/src/benches/move_gen.rs +++ b/src/benches/move_gen.rs @@ -4,8 +4,8 @@ use criterion::Criterion; use criterion::black_box; use criterion::criterion_group; use criterion::criterion_main; -use sandy_engine::search::moveordering::ordered_moves; -use sandy_engine::search::moveordering::unordered_moves; +use sandy_engine::move_generation::moveordering::ordered_moves; +use sandy_engine::move_generation::moveordering::unordered_moves; use sandy_engine::util::bench_positions; /// Benchmark the move generation diff --git a/src/benches/move_gen_grind.rs b/src/benches/move_gen_grind.rs index e576e3a4..e10174b9 100644 --- a/src/benches/move_gen_grind.rs +++ b/src/benches/move_gen_grind.rs @@ -2,8 +2,8 @@ use chess::Board; use chess::MoveGen; use iai::black_box; -use sandy_engine::search::moveordering::ordered_moves; -use sandy_engine::search::moveordering::unordered_moves; +use sandy_engine::move_generation::moveordering::ordered_moves; +use sandy_engine::move_generation::moveordering::unordered_moves; /// how many instructions does the library need to generate moves fn lib_move_gen() { diff --git a/src/engine/evaluation/mod.rs b/src/engine/evaluation/mod.rs index f6cf0888..6f764206 100644 --- a/src/engine/evaluation/mod.rs +++ b/src/engine/evaluation/mod.rs @@ -12,11 +12,11 @@ use chess::EMPTY; use crate::evaluation::material::interpolate; use crate::evaluation::material::material; use crate::evaluation::position::piece_position_benefit_for_side; +use crate::move_generation::moveordering::MoveOrdering; use crate::optlog; use crate::opts::Opts; use crate::opts::setopts; use crate::position::Position; -use crate::search::moveordering::MoveOrdering; use crate::setup::values::Value; /// a bonus given to the side-to-move for having a tempo advantage @@ -50,8 +50,6 @@ pub fn evaluate(pos: &Position, out_of_moves: bool) -> Value { // encourage it to keep playing instead value -= material(&pos.chessboard, stm, (0.0, 0.0, 1.0)); value += material(&pos.chessboard, stm.not(), (0.0, 0.0, 1.0)); - // value is small as to not significantly impact the search tree - value.0 = value.0.checked_shr(3).unwrap_or_default(); value } else { // Side to move is checkmated diff --git a/src/engine/evaluation/tests/mod.rs b/src/engine/evaluation/tests/mod.rs index 8c441914..3afef4ea 100644 --- a/src/engine/evaluation/tests/mod.rs +++ b/src/engine/evaluation/tests/mod.rs @@ -3,31 +3,15 @@ use std::str::FromStr; use chess::Board; use crate::evaluation; +use crate::move_generation::moveordering::ordered_moves; use crate::opts::Opts; -use crate::search::moveordering::ordered_moves; use crate::setup::values::Value; -// #[test] -// fn startpos_is_tempo() { -// let pos = Board::default(); -// let moves = ordered_moves(&pos); -// assert_eq!( -// evaluation::evaluate(&pos, &moves, DbOpt { debug: true }), -// TEMPO -// ); -// } - -// #[test] -// fn mate_is_mate() { -// let pos = Board::from_str("8/8/8/8/8/8/8/5KQk b - - 0 1").unwrap(); -// let moves = ordered_moves(&pos); -// assert_eq!( -// evaluation::evaluate(&pos, &moves, DbOpt { debug: true }), -// -Value::MATE, -// "{}", -// pos.print() -// ); -// } +#[test] +fn mate_is_mate() { + let pos = Board::from_str("8/8/8/8/8/8/8/5KQk b - - 0 1").unwrap(); + assert_eq!(evaluation::evaluate(&pos.into(), true), -Value::MATE); +} #[test] fn white_completely_winning() { diff --git a/src/engine/move_generation/mod.rs b/src/engine/move_generation/mod.rs index a2391b94..d7be9cde 100644 --- a/src/engine/move_generation/mod.rs +++ b/src/engine/move_generation/mod.rs @@ -1,11 +1,8 @@ //! move generation utilities -#![allow(unused)] // TODO: remove +pub mod moveordering; +pub mod mv_heuristics; use std::fmt::Debug; -use std::fmt::Display; -use std::ops::BitAnd; -use std::ops::Not; -use std::str::FromStr; use chess::BitBoard; use chess::Board; diff --git a/src/engine/search/moveordering.rs b/src/engine/move_generation/moveordering.rs similarity index 97% rename from src/engine/search/moveordering.rs rename to src/engine/move_generation/moveordering.rs index eee59acd..2936f9a8 100644 --- a/src/engine/search/moveordering.rs +++ b/src/engine/move_generation/moveordering.rs @@ -9,7 +9,7 @@ use chess::Board; use chess::ChessMove; use chess::MoveGen; -use crate::search::mv_heuristics::move_gen_ordering; +use super::mv_heuristics::move_gen_ordering; /// A struct that holds a vector of moves, ordered by importance #[derive(Debug)] diff --git a/src/engine/search/mv_heuristics.rs b/src/engine/move_generation/mv_heuristics.rs similarity index 100% rename from src/engine/search/mv_heuristics.rs rename to src/engine/move_generation/mv_heuristics.rs diff --git a/src/engine/search/tests/moveordering.rs b/src/engine/move_generation/tests/moveordering.rs similarity index 96% rename from src/engine/search/tests/moveordering.rs rename to src/engine/move_generation/tests/moveordering.rs index 647e1a28..f7138433 100644 --- a/src/engine/search/tests/moveordering.rs +++ b/src/engine/move_generation/tests/moveordering.rs @@ -4,12 +4,12 @@ use std::time::Instant; use chess::Board; use chess::MoveGen; +use crate::move_generation::moveordering::ordered_moves; +use crate::move_generation::moveordering::pv_ordered_moves; +use crate::move_generation::moveordering::unordered_moves; use crate::move_generation::prio_iterator; use crate::opts::Opts; use crate::opts::setopts; -use crate::search::moveordering::ordered_moves; -use crate::search::moveordering::pv_ordered_moves; -use crate::search::moveordering::unordered_moves; #[test] fn ordered_same_as_mg() { diff --git a/src/engine/search/mod.rs b/src/engine/search/mod.rs index 8922fdc9..7b69c8c1 100644 --- a/src/engine/search/mod.rs +++ b/src/engine/search/mod.rs @@ -1,7 +1,5 @@ //! The search module contains the search logic for the engine. mod main_search; -pub mod moveordering; -pub mod mv_heuristics; pub mod negamax; use std::fmt::Display; diff --git a/src/engine/search/tests/negatest.rs b/src/engine/search/tests/negatest.rs index f4564002..383e0b38 100644 --- a/src/engine/search/tests/negatest.rs +++ b/src/engine/search/tests/negatest.rs @@ -8,11 +8,11 @@ use chess::Color; use crate::Engine; use crate::debug::DebugLevel::debug; +use crate::move_generation::moveordering::ordered_moves; use crate::opts::opts; use crate::opts::setopts; use crate::position::Position; use crate::search::SEARCHING; -use crate::search::moveordering::ordered_moves; use crate::search::negamax::Opts; use crate::search::negamax::ng_test; use crate::setup::depth::Depth; diff --git a/src/sandy/player/parse_move.rs b/src/sandy/player/parse_move.rs index f82e4ea1..4ac93c7f 100644 --- a/src/sandy/player/parse_move.rs +++ b/src/sandy/player/parse_move.rs @@ -2,7 +2,7 @@ use anyhow::Result; use chess::Board; use chess::ChessMove; use inquire::Select; -use sandy_engine::search::moveordering::ordered_moves; +use sandy_engine::move_generation::moveordering::ordered_moves; /// Parse a player move from the terminal pub fn parse_player_move(pos: &Board) -> Result { From 3ef4ed741e3ac9bbffb11a66301877f900071dc4 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sat, 19 Apr 2025 23:52:40 +0200 Subject: [PATCH 2/9] mvv-lva --- src/engine/evaluation/material.rs | 3 ++- src/engine/move_generation/mod.rs | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/engine/evaluation/material.rs b/src/engine/evaluation/material.rs index 5210dac2..14414d41 100644 --- a/src/engine/evaluation/material.rs +++ b/src/engine/evaluation/material.rs @@ -25,12 +25,13 @@ pub const MAT_PIECE_TYPES: [Piece; 5] = [ ]; /// Initial values for each piece type. -pub const INITIAL_VALUES: [Value; 5] = [ +pub const INITIAL_VALUES: [Value; 6] = [ Value(100), // Pawn Value(290), // Knight Value(310), // Bishop Value(500), // Rook Value(900), // Queen + Value(700), // king? ]; /// Midgame values for each piece type. diff --git a/src/engine/move_generation/mod.rs b/src/engine/move_generation/mod.rs index d7be9cde..6d818027 100644 --- a/src/engine/move_generation/mod.rs +++ b/src/engine/move_generation/mod.rs @@ -9,6 +9,7 @@ use chess::Board; use chess::ChessMove; use chess::EMPTY; use chess::MoveGen; +use mv_heuristics::mvv_lva_score; use crate::evaluation::bitboards::CENTER_4; use crate::evaluation::bitboards::CENTER_16; @@ -29,8 +30,8 @@ pub struct OrderedMoves { /// /// prio_moves will have elements removed from the end, so order them from least /// important to most important -pub fn prio_iterator(mut mgen: MoveGen, pos: &Board, prio_moves: &[ChessMove]) -> OrderedMoves { - for mv in prio_moves { +pub fn prio_iterator(mut mgen: MoveGen, pos: &Board, prio: &[ChessMove]) -> OrderedMoves { + for mv in prio { mgen.remove_move(*mv); } let masks = [ @@ -44,10 +45,18 @@ pub fn prio_iterator(mut mgen: MoveGen, pos: &Board, prio_moves: &[ChessMove]) - !EMPTY, ]; + // collect capture moves and sort by mvv-lva + mgen.set_iterator_mask(*pos.color_combined(!pos.side_to_move())); + + let mut prio_moves: Vec = mgen.by_ref().collect(); + prio_moves.sort_by_cached_key(|mv| mvv_lva_score(pos, mv)); + + prio_moves.extend_from_slice(prio); + mgen.set_iterator_mask(masks[0]); OrderedMoves { - prio_moves: prio_moves.to_vec(), + prio_moves, mgen, masks, cur_mask: 0, From 0c137f64ef460ba194b899791ebeb12e18da608b Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sun, 20 Apr 2025 00:18:20 +0200 Subject: [PATCH 3/9] assert for total order --- src/benches/eval.rs | 2 +- src/benches/eval_grind.rs | 2 +- src/benches/move_gen.rs | 4 +- src/benches/move_gen_grind.rs | 4 +- src/engine/evaluation/mod.rs | 2 +- src/engine/evaluation/tests/mod.rs | 2 +- .../{mv_heuristics.rs => heuristics.rs} | 4 ++ src/engine/move_generation/mod.rs | 6 +- .../{moveordering.rs => ordering.rs} | 4 +- .../move_generation/tests/heuristics.rs | 60 +++++++++++++++++++ .../tests/{moveordering.rs => ordering.rs} | 6 +- src/engine/search/tests/negatest.rs | 2 +- src/sandy/player/parse_move.rs | 2 +- 13 files changed, 82 insertions(+), 18 deletions(-) rename src/engine/move_generation/{mv_heuristics.rs => heuristics.rs} (96%) rename src/engine/move_generation/{moveordering.rs => ordering.rs} (96%) create mode 100644 src/engine/move_generation/tests/heuristics.rs rename src/engine/move_generation/tests/{moveordering.rs => ordering.rs} (96%) diff --git a/src/benches/eval.rs b/src/benches/eval.rs index 2327224d..82947407 100644 --- a/src/benches/eval.rs +++ b/src/benches/eval.rs @@ -4,7 +4,7 @@ use criterion::Criterion; use criterion::black_box; use criterion::criterion_group; use criterion::criterion_main; -use sandy_engine::move_generation::moveordering::ordered_moves; +use sandy_engine::move_generation::ordering::ordered_moves; use sandy_engine::opts::Opts; use sandy_engine::util::bench_positions; diff --git a/src/benches/eval_grind.rs b/src/benches/eval_grind.rs index 8bc4af0f..99f1419f 100644 --- a/src/benches/eval_grind.rs +++ b/src/benches/eval_grind.rs @@ -1,7 +1,7 @@ //! This file contains valgrind benchmarks for the evaluation function. use chess::Board; use iai::black_box; -use sandy_engine::move_generation::moveordering::unordered_moves; +use sandy_engine::move_generation::ordering::unordered_moves; /// how many instructions does it take to set up a board fn board_setup() { diff --git a/src/benches/move_gen.rs b/src/benches/move_gen.rs index a6911526..8b586381 100644 --- a/src/benches/move_gen.rs +++ b/src/benches/move_gen.rs @@ -4,8 +4,8 @@ use criterion::Criterion; use criterion::black_box; use criterion::criterion_group; use criterion::criterion_main; -use sandy_engine::move_generation::moveordering::ordered_moves; -use sandy_engine::move_generation::moveordering::unordered_moves; +use sandy_engine::move_generation::ordering::ordered_moves; +use sandy_engine::move_generation::ordering::unordered_moves; use sandy_engine::util::bench_positions; /// Benchmark the move generation diff --git a/src/benches/move_gen_grind.rs b/src/benches/move_gen_grind.rs index e10174b9..e99db457 100644 --- a/src/benches/move_gen_grind.rs +++ b/src/benches/move_gen_grind.rs @@ -2,8 +2,8 @@ use chess::Board; use chess::MoveGen; use iai::black_box; -use sandy_engine::move_generation::moveordering::ordered_moves; -use sandy_engine::move_generation::moveordering::unordered_moves; +use sandy_engine::move_generation::ordering::ordered_moves; +use sandy_engine::move_generation::ordering::unordered_moves; /// how many instructions does the library need to generate moves fn lib_move_gen() { diff --git a/src/engine/evaluation/mod.rs b/src/engine/evaluation/mod.rs index 6f764206..1f6bf903 100644 --- a/src/engine/evaluation/mod.rs +++ b/src/engine/evaluation/mod.rs @@ -12,7 +12,7 @@ use chess::EMPTY; use crate::evaluation::material::interpolate; use crate::evaluation::material::material; use crate::evaluation::position::piece_position_benefit_for_side; -use crate::move_generation::moveordering::MoveOrdering; +use crate::move_generation::ordering::MoveOrdering; use crate::optlog; use crate::opts::Opts; use crate::opts::setopts; diff --git a/src/engine/evaluation/tests/mod.rs b/src/engine/evaluation/tests/mod.rs index 3afef4ea..8c923231 100644 --- a/src/engine/evaluation/tests/mod.rs +++ b/src/engine/evaluation/tests/mod.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use chess::Board; use crate::evaluation; -use crate::move_generation::moveordering::ordered_moves; +use crate::move_generation::ordering::ordered_moves; use crate::opts::Opts; use crate::setup::values::Value; diff --git a/src/engine/move_generation/mv_heuristics.rs b/src/engine/move_generation/heuristics.rs similarity index 96% rename from src/engine/move_generation/mv_heuristics.rs rename to src/engine/move_generation/heuristics.rs index 0de5db44..5fce1895 100644 --- a/src/engine/move_generation/mv_heuristics.rs +++ b/src/engine/move_generation/heuristics.rs @@ -43,3 +43,7 @@ pub fn mvv_lva_score(b: &Board, mv: &ChessMove) -> Value { _ => Value::ZERO, } } + +#[cfg(test)] +#[path = "./tests/heuristics.rs"] +mod tests; diff --git a/src/engine/move_generation/mod.rs b/src/engine/move_generation/mod.rs index 6d818027..8c0056ee 100644 --- a/src/engine/move_generation/mod.rs +++ b/src/engine/move_generation/mod.rs @@ -1,7 +1,7 @@ //! move generation utilities -pub mod moveordering; -pub mod mv_heuristics; +pub mod heuristics; +pub mod ordering; use std::fmt::Debug; use chess::BitBoard; @@ -9,7 +9,7 @@ use chess::Board; use chess::ChessMove; use chess::EMPTY; use chess::MoveGen; -use mv_heuristics::mvv_lva_score; +use heuristics::mvv_lva_score; use crate::evaluation::bitboards::CENTER_4; use crate::evaluation::bitboards::CENTER_16; diff --git a/src/engine/move_generation/moveordering.rs b/src/engine/move_generation/ordering.rs similarity index 96% rename from src/engine/move_generation/moveordering.rs rename to src/engine/move_generation/ordering.rs index 2936f9a8..3c2168f4 100644 --- a/src/engine/move_generation/moveordering.rs +++ b/src/engine/move_generation/ordering.rs @@ -9,7 +9,7 @@ use chess::Board; use chess::ChessMove; use chess::MoveGen; -use super::mv_heuristics::move_gen_ordering; +use super::heuristics::move_gen_ordering; /// A struct that holds a vector of moves, ordered by importance #[derive(Debug)] @@ -93,5 +93,5 @@ impl IntoIterator for MoveOrdering { } #[cfg(test)] -#[path = "./tests/moveordering.rs"] +#[path = "./tests/ordering.rs"] mod tests; diff --git a/src/engine/move_generation/tests/heuristics.rs b/src/engine/move_generation/tests/heuristics.rs new file mode 100644 index 00000000..aa304526 --- /dev/null +++ b/src/engine/move_generation/tests/heuristics.rs @@ -0,0 +1,60 @@ +//! tests for the heuristics + +use chess::ChessMove; +use chess::MoveGen; + +use super::mvv_lva_score; +use crate::util::bench_positions; + +#[test] +fn assert_total_order() { + let boards = bench_positions(); + + for b in boards { + let mut mgen = MoveGen::new_legal(&b); + mgen.set_iterator_mask(*b.color_combined(!b.side_to_move())); + let moves: Vec = mgen.collect(); + + let mvv_lva = |mv| mvv_lva_score(&b, mv); + + for i in 0..moves.len() { + for j in 0..moves.len() { + let a = mvv_lva(&moves[i]); + let b = mvv_lva(&moves[j]); + + // Total order requires: + // 1. a <= b || a >= b (comparability) + // 2. if a == b then b == a (symmetry) + // 3. if a <= b && b <= c then a <= c (transitivity) - checked separately + + assert!(a <= b || a >= b, "Incomparable: {:?} vs {:?}", a, b); + if a == b { + assert_eq!(b, a, "Equality not symmetric: {:?} vs {:?}", a, b); + } + } + } + + // Check transitivity separately + for i in 0..moves.len() { + for j in 0..moves.len() { + for k in 0..moves.len() { + let a = mvv_lva(&moves[i]); + let b = mvv_lva(&moves[j]); + let c = mvv_lva(&moves[k]); + + if a <= b && b <= c { + assert!( + a <= c, + "Transitivity failed: {:?} <= {:?} <= {:?} but {:?} !<= {:?}", + a, + b, + c, + a, + c + ); + } + } + } + } + } +} diff --git a/src/engine/move_generation/tests/moveordering.rs b/src/engine/move_generation/tests/ordering.rs similarity index 96% rename from src/engine/move_generation/tests/moveordering.rs rename to src/engine/move_generation/tests/ordering.rs index f7138433..0e137cc6 100644 --- a/src/engine/move_generation/tests/moveordering.rs +++ b/src/engine/move_generation/tests/ordering.rs @@ -4,9 +4,9 @@ use std::time::Instant; use chess::Board; use chess::MoveGen; -use crate::move_generation::moveordering::ordered_moves; -use crate::move_generation::moveordering::pv_ordered_moves; -use crate::move_generation::moveordering::unordered_moves; +use crate::move_generation::ordering::ordered_moves; +use crate::move_generation::ordering::pv_ordered_moves; +use crate::move_generation::ordering::unordered_moves; use crate::move_generation::prio_iterator; use crate::opts::Opts; use crate::opts::setopts; diff --git a/src/engine/search/tests/negatest.rs b/src/engine/search/tests/negatest.rs index 383e0b38..2f1bd9bd 100644 --- a/src/engine/search/tests/negatest.rs +++ b/src/engine/search/tests/negatest.rs @@ -8,7 +8,7 @@ use chess::Color; use crate::Engine; use crate::debug::DebugLevel::debug; -use crate::move_generation::moveordering::ordered_moves; +use crate::move_generation::ordering::ordered_moves; use crate::opts::opts; use crate::opts::setopts; use crate::position::Position; diff --git a/src/sandy/player/parse_move.rs b/src/sandy/player/parse_move.rs index 4ac93c7f..b158fc47 100644 --- a/src/sandy/player/parse_move.rs +++ b/src/sandy/player/parse_move.rs @@ -2,7 +2,7 @@ use anyhow::Result; use chess::Board; use chess::ChessMove; use inquire::Select; -use sandy_engine::move_generation::moveordering::ordered_moves; +use sandy_engine::move_generation::ordering::ordered_moves; /// Parse a player move from the terminal pub fn parse_player_move(pos: &Board) -> Result { From d210b2514635dd1c6d3f7c108ddaa0dd8aa8de1b Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sun, 20 Apr 2025 01:49:23 +0200 Subject: [PATCH 4/9] better threefold prevention --- src/engine/evaluation/material.rs | 11 +++++++ src/engine/evaluation/position.rs | 24 +++++++++++++++ src/engine/position.rs | 4 +-- src/engine/search/main_search.rs | 15 +++++++++- src/engine/search/mod.rs | 3 ++ src/engine/search/negamax.rs | 46 +++++++++++++++++++++-------- src/engine/search/tests/negatest.rs | 8 ++--- 7 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/engine/evaluation/material.rs b/src/engine/evaluation/material.rs index 14414d41..f23e714b 100644 --- a/src/engine/evaluation/material.rs +++ b/src/engine/evaluation/material.rs @@ -60,6 +60,17 @@ pub fn material(board: &Board, side: Color, interp: Interp) -> Value { let side_board = board.color_combined(side); for (idx, piece) in MAT_PIECE_TYPES.iter().enumerate() { let count = board.pieces(*piece).bitand(side_board).popcnt(); + // let blocked = if side == board.side_to_move() { + // board.pieces(*piece).bitand(board.pinned()).popcnt() + // } else { + // board + // .null_move() + // .map(|b| b.pieces(*piece).bitand(b.pinned()).popcnt()) + // // .unwrap_or(0) + // .unwrap_or(board.color_combined(!side).popcnt()) + // }; + // let adj = count.saturating_add(count).saturating_sub(blocked).checked_shr(1). + // unwrap_or_default(); value += (INITIAL_VALUES[idx] * Value::from(count)) * interp.0; value += (MIDGAME_VALUES[idx] * Value::from(count)) * interp.1; value += (ENDGAME_VALUES[idx] * Value::from(count)) * interp.2; diff --git a/src/engine/evaluation/position.rs b/src/engine/evaluation/position.rs index fd9f3710..5e5dceb4 100644 --- a/src/engine/evaluation/position.rs +++ b/src/engine/evaluation/position.rs @@ -1,8 +1,10 @@ //! evaluating the positions of the pieces on the board use std::ops::BitAnd; +use chess::BitBoard; use chess::Board; use chess::Color; +use chess::Piece; use chess::Square; use crate::evaluation::Interp; @@ -11,6 +13,11 @@ use crate::evaluation::bitboards::MG_PESTO_TABLE; use crate::evaluation::bitboards::POS_PIECE_TYPES; use crate::setup::values::Value; +/// bitboard of dark squares +const DARK_SQUARES: BitBoard = BitBoard(0xAA55AA55AA55AA55); +/// bitboard of light squares +const LIGHT_SQUARES: BitBoard = BitBoard(0x55AA55AA55AA55AA); + /// returns the benefit this side has from its pieces' positions pub fn piece_position_benefit_for_side(pos: &Board, color: Color, interp: Interp) -> Value { let mut value = Value::ZERO; @@ -48,6 +55,23 @@ pub fn sq_pi(sq: Square, color: Color) -> (usize, usize) { (rank, file) } +/// check how much are my bishops blocked by other pieces. +/// +/// issue: engine sacks bishops to minimise penalty +#[allow(dead_code)] +pub fn bishop_penalty(pos: &Board, side: Color) -> Value { + let bishop_squares = pos.pieces(Piece::Bishop) & pos.color_combined(side); + let mut penalty = Value::ZERO; + + let dark_bishops = bishop_squares.bitand(DARK_SQUARES); + let light_bishops = bishop_squares.bitand(LIGHT_SQUARES); + + penalty += Value::from((pos.combined() & DARK_SQUARES).popcnt() * dark_bishops.popcnt()); + penalty += Value::from((pos.combined() & LIGHT_SQUARES).popcnt() * light_bishops.popcnt()); + + penalty +} + #[cfg(test)] #[path = "tests/positions.rs"] mod tests; diff --git a/src/engine/position.rs b/src/engine/position.rs index 3a1a9bde..63d486d6 100644 --- a/src/engine/position.rs +++ b/src/engine/position.rs @@ -27,10 +27,10 @@ impl Position { /// check whether this position, if added to the engine history, will cause /// a draw by threefold repetition - pub fn causes_threefold(&self, history: &[Position]) -> bool { + pub fn causes_threefold(&self, history: &[u64]) -> bool { history .iter() - .filter(|p| p.chessboard.eq(&self.chessboard)) + .filter(|p| **p == self.chessboard.get_hash()) .count() >= 2 } diff --git a/src/engine/search/main_search.rs b/src/engine/search/main_search.rs index dcd51d48..0f332a96 100644 --- a/src/engine/search/main_search.rs +++ b/src/engine/search/main_search.rs @@ -56,7 +56,12 @@ impl Engine { let tt = self.table.get(); - let engine_history = self.history.make_contiguous().to_vec(); + let engine_history = self + .history + .make_contiguous() + .iter_mut() + .map(|p| p.chessboard.get_hash()) + .collect::>(); thread::spawn(move || { let mut best_move: Option = None; @@ -75,6 +80,14 @@ impl Engine { let initial_options = SearchOptions { extensions: Depth::ZERO, + history: [ + *engine_history.first().unwrap_or(&0), + *engine_history.get(1).unwrap_or(&0), + *engine_history.get(2).unwrap_or(&0), + *engine_history.get(3).unwrap_or(&0), + *engine_history.get(4).unwrap_or(&0), + *engine_history.get(5).unwrap_or(&0), + ], }; // SAFETY: if it fails it's due to poison, diff --git a/src/engine/search/mod.rs b/src/engine/search/mod.rs index 7b69c8c1..b39ce6b1 100644 --- a/src/engine/search/mod.rs +++ b/src/engine/search/mod.rs @@ -109,6 +109,9 @@ pub struct SearchOptions { /// how many times have we already extended the search? this is necessary to /// ensure the recursion terminates, and to prevent stack overflow. pub extensions: Depth, + + /// previously played position that would cause draw by threefold repetition + pub history: [u64; 6], } /// wrapper around [`SEARCH_UNTIL`] diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index f3abf638..64c88b2f 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -106,21 +106,39 @@ pub fn negamax( optlog!(search;trace;"ng: {pos}, td: {to_depth:?}, a: {alpha:?}, b: {beta:?}"); + let current_hash = pos.chessboard.get_hash(); + if search_options + .history + .iter() + .filter(|x| **x == current_hash) + .count() + >= 2 + { + // threefold repetition + let ev = evaluate(&pos, true); + return SearchResult { + pv: vec![], + next_position_value: ev, + nodes_searched: 1, + tb_hits: 0, + depth: ONE_PLY, + }; + } + /* source: https://en.wikipedia.org/wiki/Negamax */ let alpha_orig = alpha; if opts.use_tt { - let current_hash = pos.chessboard.get_hash(); // change - if let Ok(Some(tt_entry)) = table.read().map(|l| l.get(current_hash)) - && tt_entry.is_valid() - { - if tt_entry.depth() >= to_depth { - match tt_entry.bound() { - EvalBound::Exact => return tt_entry.search_result(), - EvalBound::LowerBound => { - alpha = alpha.max(tt_entry.search_result().next_position_value) - } - EvalBound::UpperBound => { - beta = beta.min(tt_entry.search_result().next_position_value) + if let Ok(Some(tt_entry)) = table.read().map(|l| l.get(current_hash)) { + if tt_entry.is_valid() { + if tt_entry.depth() >= to_depth { + match tt_entry.bound() { + EvalBound::Exact => return tt_entry.search_result(), + EvalBound::LowerBound => { + alpha = alpha.max(tt_entry.search_result().next_position_value) + } + EvalBound::UpperBound => { + beta = beta.min(tt_entry.search_result().next_position_value) + } } } if alpha >= beta { @@ -161,11 +179,13 @@ pub fn negamax( // if theres 3 moves or less, search +1 level deeper }; + search_options.history.rotate_left(1); + search_options.history[5] = current_hash; search_options = SearchOptions { extensions: search_options .extensions .max(search_options.extensions + next_depth + 1 - to_depth), - // ..search_options + history: search_options.history, }; let mut best = None; diff --git a/src/engine/search/tests/negatest.rs b/src/engine/search/tests/negatest.rs index 2f1bd9bd..0157cc84 100644 --- a/src/engine/search/tests/negatest.rs +++ b/src/engine/search/tests/negatest.rs @@ -24,11 +24,11 @@ use crate::util::short_benches; fn startpos_is_positive() { let pos = Board::default(); SEARCHING.store(true, Ordering::Relaxed); + let val = ng_test(pos, Depth(4), Value::MIN, Value::MAX, Opts::new()).unwrap(); assert!( - ng_test(pos, Depth(4), Value::MIN, Value::MAX, Opts::new()) - .unwrap() - .next_position_value - > Value::ZERO + val.next_position_value > Value::ZERO, + "startpos was {}", + val.next_position_value ); } From 6c8d86ad3e3cb923bc557fd30502db1912c47284 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sun, 20 Apr 2025 01:53:51 +0200 Subject: [PATCH 5/9] remove mvv-lva --- src/engine/move_generation/mod.rs | 17 ++++++++--------- src/engine/search/negamax.rs | 16 ++++++++-------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/engine/move_generation/mod.rs b/src/engine/move_generation/mod.rs index 8c0056ee..c24e1d25 100644 --- a/src/engine/move_generation/mod.rs +++ b/src/engine/move_generation/mod.rs @@ -9,7 +9,6 @@ use chess::Board; use chess::ChessMove; use chess::EMPTY; use chess::MoveGen; -use heuristics::mvv_lva_score; use crate::evaluation::bitboards::CENTER_4; use crate::evaluation::bitboards::CENTER_16; @@ -45,18 +44,18 @@ pub fn prio_iterator(mut mgen: MoveGen, pos: &Board, prio: &[ChessMove]) -> Orde !EMPTY, ]; - // collect capture moves and sort by mvv-lva - mgen.set_iterator_mask(*pos.color_combined(!pos.side_to_move())); - - let mut prio_moves: Vec = mgen.by_ref().collect(); - prio_moves.sort_by_cached_key(|mv| mvv_lva_score(pos, mv)); - - prio_moves.extend_from_slice(prio); + // // collect capture moves and sort by mvv-lva + // mgen.set_iterator_mask(*pos.color_combined(!pos.side_to_move())); + // + // let mut prio_moves: Vec = mgen.by_ref().collect(); + // prio_moves.sort_by_cached_key(|mv| mvv_lva_score(pos, mv)); + // + // prio_moves.extend_from_slice(prio); mgen.set_iterator_mask(masks[0]); OrderedMoves { - prio_moves, + prio_moves: prio.to_vec(), mgen, masks, cur_mask: 0, diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index 64c88b2f..9cf2fcda 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -96,14 +96,6 @@ pub fn negamax( opts: &Opts, table: &ShareImpl, ) -> SearchResult { - // the initial move generator - let mut base_gen = MoveGen::new_legal(&pos.chessboard); - // slice for already generated moves. - let mut pre_generated: [Option; 4] = [None; 4]; - - // we are mated! - let out_of_moves = base_gen.len() == 0; - optlog!(search;trace;"ng: {pos}, td: {to_depth:?}, a: {alpha:?}, b: {beta:?}"); let current_hash = pos.chessboard.get_hash(); @@ -125,6 +117,14 @@ pub fn negamax( }; } + // the initial move generator + let mut base_gen = MoveGen::new_legal(&pos.chessboard); + // slice for already generated moves. + let mut pre_generated: [Option; 4] = [None; 4]; + + // we are mated! + let out_of_moves = base_gen.len() == 0; + /* source: https://en.wikipedia.org/wiki/Negamax */ let alpha_orig = alpha; if opts.use_tt { From a56e6cc3cc640f8dc1e322a89521110b34b61eb5 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Wed, 23 Apr 2025 13:59:19 +0200 Subject: [PATCH 6/9] stronger threefold repetition prevention --- README.md | 2 +- src/engine/evaluation/mod.rs | 14 ++++++++------ src/engine/lib.rs | 2 +- src/engine/search/main_search.rs | 3 +++ src/engine/search/mod.rs | 2 +- src/engine/search/negamax.rs | 4 ++-- src/sandy/player/mod.rs | 2 +- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 78736133..bd2a883a 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ everything under [`./src/sandy/`](src/sandy/) is part of the frontend (almost) - [`./src/sandy/uci/search_controls.rs`](src/sandy/uci/search_controls.rs) does the same for search controls (eg `go depth 4`) ## Changelog -- `v0.6.3` TBD, TODO +- `v0.6.3` one more attempt at fixing 3fold repetition avoidance - `v0.6.2` inline move ordering: switch from an allocated `Vec` to an iterator that only generates moves as needed, performing all move ordering operations on the construction of the iterator. - `v0.6.1` variable search depth: when a node has <= 3 children, increase search depth by 1, just for this case. this massively helps lookahead in positions with a lot of checks - lost versions: i did not actually keep a changelog until `v0.6.1`. i do not remember the details here diff --git a/src/engine/evaluation/mod.rs b/src/engine/evaluation/mod.rs index 1f6bf903..0d9329a6 100644 --- a/src/engine/evaluation/mod.rs +++ b/src/engine/evaluation/mod.rs @@ -46,15 +46,17 @@ pub fn evaluate(pos: &Position, out_of_moves: bool) -> Value { if out_of_moves { return if pos.chessboard.checkers().eq(&EMPTY) { optlog!(eval;debug;"eval stalemate"); - // in stalemate, give a slightly negative score to the side that's winning to + // in stalemate, give a negative score to the side that's winning to // encourage it to keep playing instead - value -= material(&pos.chessboard, stm, (0.0, 0.0, 1.0)); - value += material(&pos.chessboard, stm.not(), (0.0, 0.0, 1.0)); - value + value += material(&pos.chessboard, stm, (0.0, 0.0, 1.0)); + value -= material(&pos.chessboard, stm.not(), (0.0, 0.0, 1.0)); + value += piece_position_benefit_for_side(&pos.chessboard, stm, (0.0, 0.0, 1.0)); + value -= piece_position_benefit_for_side(&pos.chessboard, stm.not(), (0.0, 0.0, 1.0)); + -2 * value } else { // Side to move is checkmated - optlog!(eval;debug;"eval checkmate"); - -Value::MATE // Large negative value + optlog!(eval;trace;"eval checkmate"); + -Value::MATE }; } diff --git a/src/engine/lib.rs b/src/engine/lib.rs index 3a44b391..df7c89c0 100644 --- a/src/engine/lib.rs +++ b/src/engine/lib.rs @@ -71,7 +71,7 @@ impl Engine { /// positions to detect threefold repetition. pub fn log_position(&mut self, pos: Position) { self.history.push_front(pos); - self.history.truncate(6); + self.history.truncate(7); } /// set the global [`SEARCHING`] diff --git a/src/engine/search/main_search.rs b/src/engine/search/main_search.rs index 0f332a96..4c212188 100644 --- a/src/engine/search/main_search.rs +++ b/src/engine/search/main_search.rs @@ -87,9 +87,12 @@ impl Engine { *engine_history.get(3).unwrap_or(&0), *engine_history.get(4).unwrap_or(&0), *engine_history.get(5).unwrap_or(&0), + *engine_history.get(6).unwrap_or(&0), ], }; + optlog!(search;warn;"history:{:?}", initial_options.history); + // SAFETY: if it fails it's due to poison, // and that means another thread panicked, // so we should panic as well anyway diff --git a/src/engine/search/mod.rs b/src/engine/search/mod.rs index b39ce6b1..18b31c0d 100644 --- a/src/engine/search/mod.rs +++ b/src/engine/search/mod.rs @@ -111,7 +111,7 @@ pub struct SearchOptions { pub extensions: Depth, /// previously played position that would cause draw by threefold repetition - pub history: [u64; 6], + pub history: [u64; 7], } /// wrapper around [`SEARCH_UNTIL`] diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index 9cf2fcda..8406b4af 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -179,8 +179,8 @@ pub fn negamax( // if theres 3 moves or less, search +1 level deeper }; - search_options.history.rotate_left(1); - search_options.history[5] = current_hash; + search_options.history.rotate_right(1); + search_options.history[0] = current_hash; search_options = SearchOptions { extensions: search_options .extensions diff --git a/src/sandy/player/mod.rs b/src/sandy/player/mod.rs index 3534e81e..a4d565af 100644 --- a/src/sandy/player/mod.rs +++ b/src/sandy/player/mod.rs @@ -74,7 +74,7 @@ pub fn terminal_loop(mut engine: Engine) -> Result<()> { engine.best_move(search_depth, search_time)? }; let capture = engine.board.chessboard.piece_on(mv.get_dest()).is_some(); - engine.board = engine.board.make_move(mv); + engine.make_move(mv); info!("{}", engine.board.print_move(mv, capture)); From f1246f6ba453f3b08d51e2f781b11385a5faa243 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sun, 27 Apr 2025 13:55:34 +0200 Subject: [PATCH 7/9] (unoptimised) quiescence search --- research/games/quiescence_release.tournament | 16 +++ src/engine/move_generation/mod.rs | 16 +++ src/engine/search/mod.rs | 1 + src/engine/search/negamax.rs | 8 +- src/engine/search/quiescence.rs | 114 +++++++++++++++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 research/games/quiescence_release.tournament create mode 100644 src/engine/search/quiescence.rs diff --git a/research/games/quiescence_release.tournament b/research/games/quiescence_release.tournament new file mode 100644 index 00000000..ebb65665 --- /dev/null +++ b/research/games/quiescence_release.tournament @@ -0,0 +1,16 @@ +Score of _sandy_release vs s0_6_3_dev3: 6 - 0 - 2 [0.875] +... _sandy_release playing White: 3 - 0 - 1 [0.875] 4 +... _sandy_release playing Black: 3 - 0 - 1 [0.875] 4 +... White vs Black: 3 - 3 - 2 [0.500] 8 +Elo difference: 338.0 +/- nan, LOS: 99.3 %, DrawRatio: 25.0 % +SPRT: llr 0 (0.0%), lbound -inf, ubound inf +8 of 8 games finished. + +Player: _sandy_release + "Draw by 3-fold repetition": 2 + "Win: Black mates": 3 + "Win: White mates": 3 +Player: s0_6_3_dev3 + "Draw by 3-fold repetition": 2 + "Loss: Black mates": 3 + "Loss: White mates": 3 diff --git a/src/engine/move_generation/mod.rs b/src/engine/move_generation/mod.rs index c24e1d25..c2efdb43 100644 --- a/src/engine/move_generation/mod.rs +++ b/src/engine/move_generation/mod.rs @@ -13,6 +13,9 @@ use chess::MoveGen; use crate::evaluation::bitboards::CENTER_4; use crate::evaluation::bitboards::CENTER_16; +/// how many of the first masks are exclusively captures. +const CAPTURE_MASKS: usize = 5; + /// An wrapper around [`MoveGen`] that orders the moves based on some heuristics pub struct OrderedMoves { /// prioritised moves (eg PV) @@ -119,6 +122,19 @@ impl OrderedMoves { ); ret } + + /// immediately generate all capturing moves. + pub fn generate_captures(&mut self) -> Vec { + let mut result = Vec::new(); + while self.cur_mask < CAPTURE_MASKS { + if let Some(mv) = self.next() { + result.push(mv); + } else { + break; + } + } + result + } } impl Debug for OrderedMoves { diff --git a/src/engine/search/mod.rs b/src/engine/search/mod.rs index 18b31c0d..fde70ff1 100644 --- a/src/engine/search/mod.rs +++ b/src/engine/search/mod.rs @@ -1,6 +1,7 @@ //! The search module contains the search logic for the engine. mod main_search; pub mod negamax; +pub mod quiescence; use std::fmt::Display; use std::ops::Neg; diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index 8406b4af..8f01b633 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -22,6 +22,7 @@ use crate::search::MV; use crate::search::SEARCH_TO; use crate::search::SEARCHING; use crate::search::SearchResult; +use crate::search::quiescence::quiescence; use crate::setup::depth::Depth; use crate::setup::depth::ONE_PLY; use crate::setup::values::Value; @@ -150,10 +151,15 @@ pub fn negamax( } } + if to_depth == Depth::ZERO { + // leaf node → do quiescence, not raw eval + return quiescence(pos, alpha, beta, search_options, opts); + } + // ordering wrapper around the move generation iterator let mut mgen = prio_iterator(base_gen, &pos.chessboard, &[]); - if to_depth == Depth::ZERO || out_of_moves { + if out_of_moves { let ev = evaluate(&pos, out_of_moves); optlog!(search;trace;"return eval: {:?}", ev); return SearchResult { diff --git a/src/engine/search/quiescence.rs b/src/engine/search/quiescence.rs new file mode 100644 index 00000000..f0217ceb --- /dev/null +++ b/src/engine/search/quiescence.rs @@ -0,0 +1,114 @@ +//! a quiescence search implementation, used to ensure the static evaluation +//! isn't ran on active positions with lots of exchanges +//! +//! + https://en.wikipedia.org/wiki/Quiescence_search +//! + https://www.chessprogramming.org/Quiescence_Search +//! + https://www.chessprogramming.org/Horizon_Effect + +use chess::MoveGen; + +use super::MV; +use super::SearchOptions; +use super::SearchResult; +use crate::evaluation::evaluate; +use crate::move_generation::prio_iterator; +use crate::opts::Opts; +use crate::position::Position; +use crate::setup::depth::Depth; +use crate::setup::values::Value; + +/// nodes ← 1 +/// stand_pat ← EVALUATE(position, inCheck = false) +/// +/// // 1) stand-pat test +/// if stand_pat ≥ β: +/// return SearchResult(value=stand_pat, nodes=nodes) +/// +/// α ← max(α, stand_pat) +/// +/// // 2) generate only tactical moves (captures, promotions, checks-only if +/// you like) captures ← GENERATE_CAPTURES(position) +/// ORDER_MOVES(captures, heuristics) +/// +/// for move in captures: +/// childPos ← position.make_move(move) +/// // recurse, note the negation +/// childRes ← QUIESCENCE(childPos, -β, -α, searchOptions, opts) +/// nodes += childRes.nodes +/// score ← -childRes.value +/// +/// if score ≥ β: +/// return SearchResult(value=score, nodes=nodes) +/// +/// α ← max(α, score) +/// +/// return SearchResult(value=α, nodes=nodes) +pub fn quiescence( + pos: Position, + mut alpha: Value, + beta: Value, + _search_options: SearchOptions, + _opts: &Opts, +) -> SearchResult { + let mut nodes = 1; + + let mgen = MoveGen::new_legal(&pos.chessboard); + let mut pgen = prio_iterator(mgen, &pos.chessboard, &[]); + + let first_move = pgen.next(); + let stand_pat = evaluate(&pos, first_move.is_none()); + + // 1. stand-pat test + if stand_pat >= beta { + return SearchResult { + pv: Vec::new(), + next_position_value: stand_pat, + nodes_searched: nodes, + tb_hits: 0, + depth: Depth::ZERO, + }; + } + + alpha = alpha.max(stand_pat); + + // 2. generate only tactical moves + let captures = pgen.generate_captures(); + + let mut pv = None; + let mut max_depth = Depth::ZERO; + for mv in first_move.iter().chain(captures.iter()) { + let child = -quiescence(pos.make_move(*mv), -beta, -alpha, _search_options, _opts); + + max_depth = max_depth.max(child.depth); + nodes += child.nodes_searched; + + if child.next_position_value >= beta { + return SearchResult { + pv: vec![MV(*mv, child.next_position_value)], + next_position_value: child.next_position_value, + nodes_searched: nodes, + depth: max_depth + 1, + tb_hits: 0, + }; + } + + if child.next_position_value >= alpha { + alpha = child.next_position_value; + pv = Some(*mv); + } + } + + let pv_move = if let Some(pm) = pv { + vec![MV(pm, alpha)] + } else { + vec![] + }; + + SearchResult { + pv: pv_move, + next_position_value: alpha, + nodes_searched: nodes, + depth: max_depth, + tb_hits: 0, + } +} From 37aea093e1b145985498dbc890b4ca6658e2d2c9 Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Mon, 28 Apr 2025 13:19:31 +0200 Subject: [PATCH 8/9] reduce contention on Opts lock --- src/benches/iterative_deepening.rs | 2 +- src/benches/ngm_full.rs | 2 +- src/engine/engine_opts.rs | 20 ++++ src/engine/evaluation/tests/positions.rs | 8 +- src/engine/lib.rs | 6 + .../move_generation/tests/heuristics.rs | 11 +- src/engine/move_generation/tests/ordering.rs | 29 ++--- src/engine/opts.rs | 109 +++++++++++------- src/engine/search/main_search.rs | 11 +- src/engine/search/negamax.rs | 14 +-- src/engine/search/quiescence.rs | 35 ++---- src/engine/search/tests/negatest.rs | 36 +++--- src/engine/util.rs | 14 +++ src/sandy/main.rs | 9 +- src/sandy/uci/mod.rs | 3 +- tests/mate.rs | 8 +- tests/shared/mod.rs | 4 +- 17 files changed, 177 insertions(+), 144 deletions(-) create mode 100644 src/engine/engine_opts.rs diff --git a/src/benches/iterative_deepening.rs b/src/benches/iterative_deepening.rs index d841b914..b8a5a773 100644 --- a/src/benches/iterative_deepening.rs +++ b/src/benches/iterative_deepening.rs @@ -27,7 +27,7 @@ fn search_benches(c: &mut Criterion) { engine.board = (*startpos).into(); setopts(Opts::bench().tt(true)).unwrap(); - group.bench_function(format!("id_pos_{}", p_idx), |b| { + group.bench_function(format!("id_pos_{p_idx}"), |b| { engine .set_search_until(Instant::now() + Duration::from_millis(10000)) .unwrap(); diff --git a/src/benches/ngm_full.rs b/src/benches/ngm_full.rs index df63a697..b31c98d9 100644 --- a/src/benches/ngm_full.rs +++ b/src/benches/ngm_full.rs @@ -28,7 +28,7 @@ fn negamax_benches(c: &mut Criterion) { .map(Position::from) .collect::>(); - group.bench_function(format!("ngm_full_depth_{}", d_idx), |b| { + group.bench_function(format!("ngm_full_depth_{d_idx}"), |b| { b.iter(|| { for startpos in positions.iter() { // run 100 positions diff --git a/src/engine/engine_opts.rs b/src/engine/engine_opts.rs new file mode 100644 index 00000000..d3d7bd84 --- /dev/null +++ b/src/engine/engine_opts.rs @@ -0,0 +1,20 @@ +//! options specific to the current engine + +/// Options for the engine's execution +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct EngineOpts { + /// should the search use alpha beta pruning? + pub use_ab: bool, + /// should the search use principal variation search? + pub use_pv: bool, + /// should the search use transposition tables? + pub use_tt: bool, + /// should the search use move ordering? + pub use_mo: bool, + /// should the engine ponder? + pub ponder: bool, + /// how big should the transposition table be? value in **bytes** + pub hash_size: usize, + /// how many threads should the search use? + pub threads: usize, +} diff --git a/src/engine/evaluation/tests/positions.rs b/src/engine/evaluation/tests/positions.rs index 0f055103..ea75adaa 100644 --- a/src/engine/evaluation/tests/positions.rs +++ b/src/engine/evaluation/tests/positions.rs @@ -41,7 +41,7 @@ fn test_single_white_pawn() { let pos = Board::from_str("8/P7/8/2k2K2/8/8/8/8 w - - 0 1").unwrap(); let interp = interpolate(&pos); let eval = piece_position_benefit_for_side(&pos, Color::White, interp); - assert_eq!(eval, Value(178), "{}", eval); + assert_eq!(eval, Value(178), "{eval}"); } #[test] @@ -49,7 +49,7 @@ fn test_single_black_pawn() { let pos = Board::from_str("8/8/8/2k2K2/8/8/p7/8 b - - 0 1").unwrap(); let interp = interpolate(&pos); let eval = piece_position_benefit_for_side(&pos, Color::Black, interp); - assert_eq!(eval, Value(178), "{}", eval); + assert_eq!(eval, Value(178), "{eval}"); } #[test] @@ -63,9 +63,9 @@ fn check_mirror_positions() { let interp_a = interpolate(&position); let interp_b = interpolate(&mirrored); - assert_eq!(interp_a, interp_b, "{:?} {:?}", interp_a, interp_b); + assert_eq!(interp_a, interp_b, "{interp_a:?} {interp_b:?}"); let eval = piece_position_benefit_for_side(&position, Color::White, interp_a); let eval_mirrored = piece_position_benefit_for_side(&mirrored, Color::Black, interp_b); - assert_eq!(eval, eval_mirrored, "{}", eval); + assert_eq!(eval, eval_mirrored, "{eval}"); } diff --git a/src/engine/lib.rs b/src/engine/lib.rs index df7c89c0..12693b53 100644 --- a/src/engine/lib.rs +++ b/src/engine/lib.rs @@ -4,6 +4,7 @@ pub mod book; pub mod debug; +pub mod engine_opts; pub mod evaluation; pub mod move_generation; pub mod opts; @@ -24,9 +25,11 @@ use std::time::Instant; use anyhow::Result; use anyhow::anyhow; use chess::ChessMove; +use engine_opts::EngineOpts; use lockfree::channel::RecvErr; use log::info; use log::trace; +use opts::opts; use crate::position::Position; use crate::search::Message; @@ -47,6 +50,8 @@ pub struct Engine { pub table: TT, /// recently played positions. used to detect 3-fold repetition. pub history: VecDeque, + /// this instance's options + pub eng_opts: EngineOpts, } impl Engine { @@ -58,6 +63,7 @@ impl Engine { board: Default::default(), table: TT::new(), history: VecDeque::new(), + eng_opts: opts()?.engine_opts, }) } diff --git a/src/engine/move_generation/tests/heuristics.rs b/src/engine/move_generation/tests/heuristics.rs index aa304526..12d60fc8 100644 --- a/src/engine/move_generation/tests/heuristics.rs +++ b/src/engine/move_generation/tests/heuristics.rs @@ -27,9 +27,9 @@ fn assert_total_order() { // 2. if a == b then b == a (symmetry) // 3. if a <= b && b <= c then a <= c (transitivity) - checked separately - assert!(a <= b || a >= b, "Incomparable: {:?} vs {:?}", a, b); + assert!(a <= b || a >= b, "Incomparable: {a:?} vs {b:?}"); if a == b { - assert_eq!(b, a, "Equality not symmetric: {:?} vs {:?}", a, b); + assert_eq!(b, a, "Equality not symmetric: {a:?} vs {b:?}"); } } } @@ -45,12 +45,7 @@ fn assert_total_order() { if a <= b && b <= c { assert!( a <= c, - "Transitivity failed: {:?} <= {:?} <= {:?} but {:?} !<= {:?}", - a, - b, - c, - a, - c + "Transitivity failed: {a:?} <= {b:?} <= {c:?} but {a:?} !<= {c:?}", ); } } diff --git a/src/engine/move_generation/tests/ordering.rs b/src/engine/move_generation/tests/ordering.rs index 0e137cc6..c42d17ad 100644 --- a/src/engine/move_generation/tests/ordering.rs +++ b/src/engine/move_generation/tests/ordering.rs @@ -25,7 +25,7 @@ fn ordered_same_as_mg() { assert_eq!(ordered.len(), mg.len()); for (i, m) in ordered.into_iter().enumerate() { - assert!(mg.contains(&m), "move {} not found in mg", i); + assert!(mg.contains(&m), "move {i} not found in mg"); } } } @@ -56,9 +56,9 @@ fn pv_ordered_same_as_mg() { .collect::>() .join(", ") ); - assert_eq!(pv_ordered.0.first(), Some(m), "pv: {}", pv_ordered); + assert_eq!(pv_ordered.0.first(), Some(m), "pv: {pv_ordered}"); for (i, m) in pv_ordered.0.iter().enumerate() { - assert!(mg.contains(m), "move {} not found in mg", i); + assert!(mg.contains(m), "move {i} not found in mg"); } } } @@ -91,9 +91,9 @@ fn prio_ordered_same_as_mg() { .collect::>() .join(", "), ); - assert_eq!(prio_ordered.first(), Some(*m), "pv: {:?}", prio_ordered); + assert_eq!(prio_ordered.first(), Some(*m), "pv: {prio_ordered:?}"); for (i, m) in prio_ordered.enumerate() { - assert!(mg.contains(&m), "move {} not found in mg", i); + assert!(mg.contains(&m), "move {i} not found in mg"); } } } @@ -124,9 +124,9 @@ fn prio_ordered_same_as_ordered() { "\nprio: {}, \nord: {pv_ordered}", prio_ordered.display(), ); - assert_eq!(prio_ordered.first(), Some(*m), "pv: {:?}", prio_ordered); + assert_eq!(prio_ordered.first(), Some(*m), "pv: {prio_ordered:?}"); for (i, m) in prio_ordered.enumerate() { - assert!(pv_ordered.0.contains(&m), "move {} not found in pv", i); + assert!(pv_ordered.0.contains(&m), "move {i} not found in pv"); } } } @@ -170,22 +170,13 @@ fn profile_move_ordering() { } let elapsed_d = start_d.elapsed(); - eprintln!( - "pv: {:?}, normal: {:?}, mg: {:?}, uo: {:?}", - elapsed_a, elapsed_b, elapsed_c, elapsed_d - ); + eprintln!("pv: {elapsed_a:?}, normal: {elapsed_b:?}, mg: {elapsed_c:?}, uo: {elapsed_d:?}",); assert!( elapsed_a < 2 * elapsed_b, - "pv: {:?}, normal: {:?}, mg: {:?}", - elapsed_a, - elapsed_b, - elapsed_c + "pv: {elapsed_a:?}, normal: {elapsed_b:?}, mg: {elapsed_c:?}", ); assert!( elapsed_a < 4 * elapsed_c, - "pv: {:?}, normal: {:?}, mg: {:?}", - elapsed_a, - elapsed_b, - elapsed_c + "pv: {elapsed_a:?}, normal: {elapsed_b:?}, mg: {elapsed_c:?}", ); } diff --git a/src/engine/opts.rs b/src/engine/opts.rs index f113f522..ea14fcfa 100644 --- a/src/engine/opts.rs +++ b/src/engine/opts.rs @@ -7,6 +7,7 @@ use anyhow::bail; use vampirc_uci::UciOptionConfig; use crate::debug::DebugLevel; +use crate::engine_opts::EngineOpts; use crate::optlog; use crate::search::SEARCH_THREADS; use crate::transposition_table::DEFAULT_TABLE_SIZE; @@ -87,20 +88,8 @@ pub struct Opts { pub uci: DebugLevel, /// the [`DebugLevel`] for other options pub opts: DebugLevel, - /// should the search use alpha beta pruning? - pub use_ab: bool, - /// should the search use principal variation search? - pub use_pv: bool, - /// should the search use transposition tables? - pub use_tt: bool, - /// should the search use move ordering? - pub use_mo: bool, - /// should the engine ponder? - pub ponder: bool, - /// how big should the transposition table be? value in **bytes** - pub hash_size: usize, - /// how many threads should the search use? - pub threads: usize, + /// options specific to the engine's execution + pub engine_opts: EngineOpts, } impl Opts { @@ -130,13 +119,15 @@ impl Opts { tt: DebugLevel::info, uci: DebugLevel::info, opts: DebugLevel::debug, - use_ab: false, - use_pv: false, - use_tt: false, - use_mo: false, - ponder: false, - hash_size: DEFAULT_TABLE_SIZE, - threads: 1, + engine_opts: EngineOpts { + use_ab: false, + use_pv: false, + use_tt: false, + use_mo: false, + ponder: false, + hash_size: DEFAULT_TABLE_SIZE, + threads: 1, + }, } } @@ -153,13 +144,15 @@ impl Opts { tt: DebugLevel::off, uci: DebugLevel::off, opts: DebugLevel::error, - use_ab: true, - use_pv: true, - use_tt: true, - use_mo: true, - ponder: false, - hash_size: 32 * 1024, - threads: 1, + engine_opts: EngineOpts { + use_ab: true, + use_pv: true, + use_tt: true, + use_mo: true, + ponder: false, + hash_size: 32 * 1024, + threads: 1, + }, } } @@ -254,17 +247,17 @@ impl Opts { _ => unreachable!(), }; match name { - "use_ab" => self.use_ab = parse_check("use_ab", value)?, - "use_pv" => self.use_pv = parse_check("use_pv", value)?, - "use_tt" => self.use_tt = parse_check("use_tt", value)?, - "use_mo" => self.use_mo = parse_check("use_mo", value)?, - "Ponder" => self.ponder = parse_check("Ponder", value)?, + "use_ab" => self.engine_opts.use_ab = parse_check("use_ab", value)?, + "use_pv" => self.engine_opts.use_pv = parse_check("use_pv", value)?, + "use_tt" => self.engine_opts.use_tt = parse_check("use_tt", value)?, + "use_mo" => self.engine_opts.use_mo = parse_check("use_mo", value)?, + "Ponder" => self.engine_opts.ponder = parse_check("Ponder", value)?, "bench_log" => { if parse_check("bench_log", value)? { return Ok(Self::bench() - .ab(self.use_ab) - .pv(self.use_pv) - .tt(self.use_tt)); + .ab(self.engine_opts.use_ab) + .pv(self.engine_opts.use_pv) + .tt(self.engine_opts.use_tt)); } } "search_debug" => { @@ -275,8 +268,11 @@ impl Opts { "tt_debug" => self.tt = DebugLevel::from(parse_spin("tt_debug", 0, 5, value)?), "uci_debug" => self.uci = DebugLevel::from(parse_spin("uci_debug", 0, 5, value)?), // hash input is in megabytes, according to UCI specification - "hash" => self.hash_size = 1024 * 1024 * parse_spin("hash", 0, 1024, value)? as usize, - "threads" => self.threads = parse_spin("threads", 0, 1024, value)? as usize, + "hash" => { + self.engine_opts.hash_size = + 1024 * 1024 * parse_spin("hash", 0, 1024, value)? as usize + } + "threads" => self.engine_opts.threads = parse_spin("threads", 0, 1024, value)? as usize, unknown => bail!("unknown option: {:?}", unknown), } @@ -328,30 +324,57 @@ impl Opts { /// Enable or disable alpha-beta pruning during search pub const fn ab(self, x: bool) -> Self { - Self { use_ab: x, ..self } + Self { + engine_opts: EngineOpts { + use_ab: x, + ..self.engine_opts + }, + ..self + } } /// Enable or disable the use of the principal variation during search (for /// move ordering only) pub const fn pv(self, x: bool) -> Self { - Self { use_pv: x, ..self } + Self { + engine_opts: EngineOpts { + use_pv: x, + ..self.engine_opts + }, + ..self + } } /// Enable or disable the use of the transposition table during search pub const fn tt(self, x: bool) -> Self { - Self { use_tt: x, ..self } + Self { + engine_opts: EngineOpts { + use_tt: x, + ..self.engine_opts + }, + ..self + } } /// Set the transposition table size **in kilobytes** pub const fn hash_size(self, x: usize) -> Self { Self { - hash_size: x, + engine_opts: EngineOpts { + hash_size: x, + ..self.engine_opts + }, ..self } } /// Set the number of threads to be used for the search pub const fn num_threads(self, x: usize) -> Self { - Self { threads: x, ..self } + Self { + engine_opts: EngineOpts { + threads: x, + ..self.engine_opts + }, + ..self + } } } diff --git a/src/engine/search/main_search.rs b/src/engine/search/main_search.rs index 4c212188..c50230b0 100644 --- a/src/engine/search/main_search.rs +++ b/src/engine/search/main_search.rs @@ -17,7 +17,6 @@ use crate::Engine; use crate::evaluation::evaluate; use crate::move_generation::prio_iterator; use crate::optlog; -use crate::opts::opts; use crate::search::MV; use crate::search::Message; use crate::search::RootNode; @@ -63,6 +62,11 @@ impl Engine { .map(|p| p.chessboard.get_hash()) .collect::>(); + // copy the currently set options to the search thread. + // this means options may not change in the duration of a search. + // (if they do, they will be in effect from the next search) + let search_options = self.eng_opts; + thread::spawn(move || { let mut best_move: Option = None; let mut best_value: Value = Value::MIN; @@ -93,11 +97,6 @@ impl Engine { optlog!(search;warn;"history:{:?}", initial_options.history); - // SAFETY: if it fails it's due to poison, - // and that means another thread panicked, - // so we should panic as well anyway - let search_options = opts().unwrap(); - // iterative deepening loop while !exit_condition() && target_depth < search_to() { // record the time it takes to reach this depth to see if it's worth it to go diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index 8f01b633..a8f81207 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -11,12 +11,11 @@ use chess::ChessMove; use chess::MoveGen; use super::SearchOptions; +use crate::engine_opts::EngineOpts; use crate::evaluation::evaluate; use crate::move_generation::prio_iterator; use crate::optlog; use crate::opts::Opts; -use crate::opts::opts; -use crate::opts::setopts; use crate::position::Position; use crate::search::MV; use crate::search::SEARCH_TO; @@ -55,13 +54,9 @@ pub fn ng_test( beta: Value, set_opts: Opts, ) -> Result { - { - setopts(set_opts)?; - } - let opt = opts()?; let table = TT::new(); let position = Position::from(board); - ng_bench(position, to_depth, alpha, beta, opt, &table) + ng_bench(position, to_depth, alpha, beta, set_opts, &table) } /// same as [`negamax`], but with a fixed signature to be used across benchmarks @@ -82,7 +77,7 @@ pub fn ng_bench( alpha, beta, Default::default(), - &opt, + &opt.engine_opts, &tt.get(), )) } @@ -94,7 +89,7 @@ pub fn negamax( mut alpha: Value, mut beta: Value, mut search_options: SearchOptions, - opts: &Opts, + opts: &EngineOpts, table: &ShareImpl, ) -> SearchResult { optlog!(search;trace;"ng: {pos}, td: {to_depth:?}, a: {alpha:?}, b: {beta:?}"); @@ -160,6 +155,7 @@ pub fn negamax( let mut mgen = prio_iterator(base_gen, &pos.chessboard, &[]); if out_of_moves { + //|| to_depth == Depth::ZERO { let ev = evaluate(&pos, out_of_moves); optlog!(search;trace;"return eval: {:?}", ev); return SearchResult { diff --git a/src/engine/search/quiescence.rs b/src/engine/search/quiescence.rs index f0217ceb..ef2c22a7 100644 --- a/src/engine/search/quiescence.rs +++ b/src/engine/search/quiescence.rs @@ -10,45 +10,26 @@ use chess::MoveGen; use super::MV; use super::SearchOptions; use super::SearchResult; +use crate::engine_opts::EngineOpts; use crate::evaluation::evaluate; use crate::move_generation::prio_iterator; -use crate::opts::Opts; use crate::position::Position; use crate::setup::depth::Depth; use crate::setup::values::Value; -/// nodes ← 1 -/// stand_pat ← EVALUATE(position, inCheck = false) +/// make sure that we only statically evaluate after all capture moves have been +/// played (end of piece exchange) /// -/// // 1) stand-pat test -/// if stand_pat ≥ β: -/// return SearchResult(value=stand_pat, nodes=nodes) -/// -/// α ← max(α, stand_pat) -/// -/// // 2) generate only tactical moves (captures, promotions, checks-only if -/// you like) captures ← GENERATE_CAPTURES(position) -/// ORDER_MOVES(captures, heuristics) -/// -/// for move in captures: -/// childPos ← position.make_move(move) -/// // recurse, note the negation -/// childRes ← QUIESCENCE(childPos, -β, -α, searchOptions, opts) -/// nodes += childRes.nodes -/// score ← -childRes.value -/// -/// if score ≥ β: -/// return SearchResult(value=score, nodes=nodes) -/// -/// α ← max(α, score) -/// -/// return SearchResult(value=α, nodes=nodes) +/// NOTE: this function does not search moves that put a player into check, +/// even though they are usually considered strategic (non-quiet) moves! +/// This is solely because I currently have no efficient way of generating +/// checks, while generating captures can be done independently of quiet moves. pub fn quiescence( pos: Position, mut alpha: Value, beta: Value, _search_options: SearchOptions, - _opts: &Opts, + _opts: &EngineOpts, ) -> SearchResult { let mut nodes = 1; diff --git a/src/engine/search/tests/negatest.rs b/src/engine/search/tests/negatest.rs index 0157cc84..ff10592d 100644 --- a/src/engine/search/tests/negatest.rs +++ b/src/engine/search/tests/negatest.rs @@ -5,12 +5,10 @@ use std::time::Duration; use chess::Board; use chess::BoardStatus; use chess::Color; +use chess::MoveGen; use crate::Engine; -use crate::debug::DebugLevel::debug; -use crate::move_generation::ordering::ordered_moves; -use crate::opts::opts; -use crate::opts::setopts; +use crate::move_generation::prio_iterator; use crate::position::Position; use crate::search::SEARCHING; use crate::search::negamax::Opts; @@ -67,21 +65,23 @@ fn mate_in_1_is_mate() { #[test] fn will_mate_in_1_() { let pos = Board::from_str("8/8/8/6Q1/8/8/8/5K1k w - - 0 1").unwrap(); - for d in 1..4 { + // NOTE: this test used to start at depth 1, + // but after implementing quiescence search it fails for + // depth <= 1. This is (as far as i can imagine) because + // quiescence only searches captures and not checks. See + // note in quiescence.rs for details. + for d in 2..5 { let mut engine = Engine::new().unwrap(); engine.board = pos.into(); - let mut opts = opts().unwrap(); - opts.search = debug; - opts.use_ab = false; - opts.use_pv = false; - opts.threads = 1; - { - setopts(opts).unwrap(); - } + engine.eng_opts.use_ab = false; + engine.eng_opts.use_pv = false; + engine.eng_opts.threads = 1; eprintln!( - "all possible moves: {}", - ordered_moves(&engine.board.chessboard) + "all possible moves: {:?}", + prio_iterator(MoveGen::new_legal(&pos), &pos, &[]) + .map(|cm| cm.to_string()) + .collect::>() //ordered_moves(&engine.board.chessboard) ); let mv = engine @@ -153,7 +153,8 @@ fn will_mate_in_2_() { for d in 5..6 { let mut engine = Engine::new().unwrap(); - setopts(Opts::new().tt(true).search(debug)).unwrap(); + // setopts(Opts::new().tt(true).search(debug)).unwrap(); + engine.eng_opts.use_tt = true; engine.board = pos.into(); let mv1 = engine @@ -161,7 +162,7 @@ fn will_mate_in_2_() { .unwrap(); engine.board = engine.board.make_move(mv1); - eprintln!("made first move in mating sequence: {}", mv1); + eprintln!("made first move in mating sequence: {mv1}"); assert_eq!( engine.board.chessboard.status(), @@ -225,6 +226,7 @@ fn score_same_with_or_without_ab_pv() { #[test] fn checkmate_the_author() { + // crate::util::setup_logging(); let pos = Board::from_str("1n1k4/r1pp1p2/7p/8/1p1q4/6r1/4q3/1K6 b - - 0 1").unwrap(); let mut engine = Engine::new().unwrap(); engine.board = pos.into(); diff --git a/src/engine/util.rs b/src/engine/util.rs index a0f965a7..f9ef49e7 100644 --- a/src/engine/util.rs +++ b/src/engine/util.rs @@ -256,3 +256,17 @@ impl Print for Board { Position::from(*self).print_move(mv, capture) } } + +/// set up logging +pub fn setup_logging() { + colog::basic_builder() + .filter( + None, + if cfg!(test) { + log::LevelFilter::Trace + } else { + log::LevelFilter::Info + }, + ) + .init(); +} diff --git a/src/sandy/main.rs b/src/sandy/main.rs index ecf82382..33b401bc 100644 --- a/src/sandy/main.rs +++ b/src/sandy/main.rs @@ -41,7 +41,14 @@ fn main() -> Result<()> { ); colog::basic_builder() - .filter(None, log::LevelFilter::Info) + .filter( + None, + if cfg!(test) { + log::LevelFilter::Trace + } else { + log::LevelFilter::Info + }, + ) .init(); // take the default panic hook, and make sure that the *entire* process is diff --git a/src/sandy/uci/mod.rs b/src/sandy/uci/mod.rs index c0634257..033db91a 100644 --- a/src/sandy/uci/mod.rs +++ b/src/sandy/uci/mod.rs @@ -71,7 +71,8 @@ pub fn uci_loop(mut engine: Engine) -> Result<()> { Err(e) => optlog!(uci;error;"error setting option: {}", e), Ok(opt) => { setopts(opt)?; - let entry_count = engine.resize_table(opt.hash_size)?; + engine.eng_opts = opt.engine_opts; + let entry_count = engine.resize_table(engine.eng_opts.hash_size)?; println!("info string table resized to {entry_count} entries."); optlog!(uci;info; diff --git a/tests/mate.rs b/tests/mate.rs index 2c294cc9..158cc33c 100644 --- a/tests/mate.rs +++ b/tests/mate.rs @@ -36,7 +36,7 @@ fn test_mating(startpos: &str, valid_mates: &[&str]) { let mut cmd = Command::new(exec); - let start_command = format!("position fen {}", startpos); + let start_command = format!("position fen {startpos}"); let sequence = [ "uci", @@ -85,7 +85,7 @@ fn test_mating(startpos: &str, valid_mates: &[&str]) { let parts = line.split_whitespace().collect::>(); if parts.len() > 1 && parts[0] == "bestmove" { best_move = parts[1].to_string(); - println!("Best move: {}", best_move); + println!("Best move: {best_move}"); break; } else if parts.len() > 1 && parts[0] == "info" { println!( @@ -106,9 +106,7 @@ fn test_mating(startpos: &str, valid_mates: &[&str]) { // we assert that the engine takes the mate-in-one assert!( valid_mates.iter().any(|x| *x == best_move), - "did not find any of the mates [{:?}], instead picked {}", - valid_mates, - best_move + "did not find any of the mates [{valid_mates:?}], instead picked {best_move}", ); child.kill().unwrap(); diff --git a/tests/shared/mod.rs b/tests/shared/mod.rs index ac65d3e4..6b3bf7ad 100644 --- a/tests/shared/mod.rs +++ b/tests/shared/mod.rs @@ -62,14 +62,14 @@ pub fn test_uci(sequence: &[&str]) { ); println!( "{}", - format!(" max depth: {}", depth) + format!(" max depth: {depth}") .black() .bold() .on_bright_green() ); println!( "{}", - format!(" total nodes searched: {}", nodes) + format!(" total nodes searched: {nodes}") .black() .bold() .on_bright_green() From b9dec812c47d0e82fc52dc2b5f831458b715316b Mon Sep 17 00:00:00 2001 From: Andreas Tsatsanis Date: Sun, 11 May 2025 13:22:36 +0200 Subject: [PATCH 9/9] uci changes Signed-off-by: Andreas Tsatsanis --- README.md | 2 +- src/engine/evaluation/mod.rs | 11 ++++++----- src/engine/opts.rs | 9 +++++++-- src/engine/search/negamax.rs | 34 +++++++++++++++++----------------- src/sandy/uci/mod.rs | 11 +++++++++-- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index bd2a883a..9d8583e4 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ everything under [`./src/sandy/`](src/sandy/) is part of the frontend (almost) - [`./src/sandy/uci/search_controls.rs`](src/sandy/uci/search_controls.rs) does the same for search controls (eg `go depth 4`) ## Changelog -- `v0.6.3` one more attempt at fixing 3fold repetition avoidance +- `v0.6.3` quiescence search, breaking changes on `Opts`, one more attempt at fixing 3fold repetition avoidance - `v0.6.2` inline move ordering: switch from an allocated `Vec` to an iterator that only generates moves as needed, performing all move ordering operations on the construction of the iterator. - `v0.6.1` variable search depth: when a node has <= 3 children, increase search depth by 1, just for this case. this massively helps lookahead in positions with a lot of checks - lost versions: i did not actually keep a changelog until `v0.6.1`. i do not remember the details here diff --git a/src/engine/evaluation/mod.rs b/src/engine/evaluation/mod.rs index 0d9329a6..c268868c 100644 --- a/src/engine/evaluation/mod.rs +++ b/src/engine/evaluation/mod.rs @@ -48,11 +48,12 @@ pub fn evaluate(pos: &Position, out_of_moves: bool) -> Value { optlog!(eval;debug;"eval stalemate"); // in stalemate, give a negative score to the side that's winning to // encourage it to keep playing instead - value += material(&pos.chessboard, stm, (0.0, 0.0, 1.0)); - value -= material(&pos.chessboard, stm.not(), (0.0, 0.0, 1.0)); - value += piece_position_benefit_for_side(&pos.chessboard, stm, (0.0, 0.0, 1.0)); - value -= piece_position_benefit_for_side(&pos.chessboard, stm.not(), (0.0, 0.0, 1.0)); - -2 * value + let interp = interpolate(&pos.chessboard); + value += material(&pos.chessboard, stm, interp); + value -= material(&pos.chessboard, stm.not(), interp); + value += piece_position_benefit_for_side(&pos.chessboard, stm, interp); + value -= piece_position_benefit_for_side(&pos.chessboard, stm.not(), interp); + -2 * (value + TEMPO + TEMPO) } else { // Side to move is checkmated optlog!(eval;trace;"eval checkmate"); diff --git a/src/engine/opts.rs b/src/engine/opts.rs index ea14fcfa..c7693f2d 100644 --- a/src/engine/opts.rs +++ b/src/engine/opts.rs @@ -12,6 +12,11 @@ use crate::optlog; use crate::search::SEARCH_THREADS; use crate::transposition_table::DEFAULT_TABLE_SIZE; +/// limit transposition table size to 64gb. +/// this limit depends on the currenly used implementation for the hash table, +/// as unfortunately bigger isn't always better. +const MAX_HASH_SIZE: i64 = 65536; + /// Read the global options for the engine, attempting to go through the /// [`RwLock`] of [`OPTS`] to do so #[inline(always)] @@ -217,7 +222,7 @@ impl Opts { name: "hash".to_string(), default: Some((DEFAULT_TABLE_SIZE / (1024 * 1024)).max(1) as i64), min: Some(0), - max: Some(4096), + max: Some(MAX_HASH_SIZE), }, UciOptionConfig::Spin { name: "threads".to_string(), @@ -270,7 +275,7 @@ impl Opts { // hash input is in megabytes, according to UCI specification "hash" => { self.engine_opts.hash_size = - 1024 * 1024 * parse_spin("hash", 0, 1024, value)? as usize + 1024 * 1024 * parse_spin("hash", 0, MAX_HASH_SIZE, value)? as usize } "threads" => self.engine_opts.threads = parse_spin("threads", 0, 1024, value)? as usize, unknown => bail!("unknown option: {:?}", unknown), diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index a8f81207..5588fd26 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -123,27 +123,27 @@ pub fn negamax( /* source: https://en.wikipedia.org/wiki/Negamax */ let alpha_orig = alpha; - if opts.use_tt { - if let Ok(Some(tt_entry)) = table.read().map(|l| l.get(current_hash)) { - if tt_entry.is_valid() { - if tt_entry.depth() >= to_depth { - match tt_entry.bound() { - EvalBound::Exact => return tt_entry.search_result(), - EvalBound::LowerBound => { - alpha = alpha.max(tt_entry.search_result().next_position_value) - } - EvalBound::UpperBound => { - beta = beta.min(tt_entry.search_result().next_position_value) - } + if opts.use_tt + && let Ok(Some(tt_entry)) = table.read().map(|l| l.get(current_hash)) + { + if tt_entry.is_valid() { + if tt_entry.depth() >= to_depth { + match tt_entry.bound() { + EvalBound::Exact => return tt_entry.search_result(), + EvalBound::LowerBound => { + alpha = alpha.max(tt_entry.search_result().next_position_value) + } + EvalBound::UpperBound => { + beta = beta.min(tt_entry.search_result().next_position_value) } } - if alpha >= beta { - return tt_entry.search_result(); - } } - pre_generated[0] = Some(tt_entry.mv()); - base_gen.remove_move(tt_entry.mv()); + if alpha >= beta { + return tt_entry.search_result(); + } } + pre_generated[0] = Some(tt_entry.mv()); + base_gen.remove_move(tt_entry.mv()); } if to_depth == Depth::ZERO { diff --git a/src/sandy/uci/mod.rs b/src/sandy/uci/mod.rs index 033db91a..e15db2d5 100644 --- a/src/sandy/uci/mod.rs +++ b/src/sandy/uci/mod.rs @@ -72,8 +72,15 @@ pub fn uci_loop(mut engine: Engine) -> Result<()> { Ok(opt) => { setopts(opt)?; engine.eng_opts = opt.engine_opts; - let entry_count = engine.resize_table(engine.eng_opts.hash_size)?; - println!("info string table resized to {entry_count} entries."); + if name == "hash" { + println!("info string resizing table..."); + let start_alloc_time = Instant::now(); + let entry_count = engine.resize_table(engine.eng_opts.hash_size)?; + println!( + "info string table resized to {entry_count} entries in {}s", + start_alloc_time.elapsed().as_secs_f32() + ); + } optlog!(uci;info; "option {name} set to {}.",