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..9d8583e4 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` 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/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/benches/eval.rs b/src/benches/eval.rs index c2e323d8..82947407 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::ordering::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..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::search::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/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/move_gen.rs b/src/benches/move_gen.rs index 1a669b40..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::search::moveordering::ordered_moves; -use sandy_engine::search::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 e576e3a4..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::search::moveordering::ordered_moves; -use sandy_engine::search::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/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/material.rs b/src/engine/evaluation/material.rs index 5210dac2..f23e714b 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. @@ -59,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/mod.rs b/src/engine/evaluation/mod.rs index f6cf0888..c268868c 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::ordering::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 @@ -46,17 +46,18 @@ 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 is small as to not significantly impact the search tree - value.0 = value.0.checked_shr(3).unwrap_or_default(); - 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;debug;"eval checkmate"); - -Value::MATE // Large negative value + optlog!(eval;trace;"eval checkmate"); + -Value::MATE }; } 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/evaluation/tests/mod.rs b/src/engine/evaluation/tests/mod.rs index 8c441914..8c923231 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::ordering::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/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 3a44b391..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, }) } @@ -71,7 +77,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/mv_heuristics.rs b/src/engine/move_generation/heuristics.rs similarity index 96% rename from src/engine/search/mv_heuristics.rs rename to src/engine/move_generation/heuristics.rs index 0de5db44..5fce1895 100644 --- a/src/engine/search/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 a2391b94..c2efdb43 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 heuristics; +pub mod ordering; 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; @@ -16,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) @@ -32,8 +32,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 = [ @@ -47,10 +47,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: prio.to_vec(), mgen, masks, cur_mask: 0, @@ -114,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/moveordering.rs b/src/engine/move_generation/ordering.rs similarity index 96% rename from src/engine/search/moveordering.rs rename to src/engine/move_generation/ordering.rs index eee59acd..3c2168f4 100644 --- a/src/engine/search/moveordering.rs +++ b/src/engine/move_generation/ordering.rs @@ -9,7 +9,7 @@ use chess::Board; use chess::ChessMove; use chess::MoveGen; -use crate::search::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..12d60fc8 --- /dev/null +++ b/src/engine/move_generation/tests/heuristics.rs @@ -0,0 +1,55 @@ +//! 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: {a:?} vs {b:?}"); + if a == b { + assert_eq!(b, a, "Equality not symmetric: {a:?} vs {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: {a:?} <= {b:?} <= {c:?} but {a:?} !<= {c:?}", + ); + } + } + } + } + } +} diff --git a/src/engine/search/tests/moveordering.rs b/src/engine/move_generation/tests/ordering.rs similarity index 82% rename from src/engine/search/tests/moveordering.rs rename to src/engine/move_generation/tests/ordering.rs index 647e1a28..c42d17ad 100644 --- a/src/engine/search/tests/moveordering.rs +++ b/src/engine/move_generation/tests/ordering.rs @@ -4,12 +4,12 @@ use std::time::Instant; use chess::Board; use chess::MoveGen; +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; -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() { @@ -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..c7693f2d 100644 --- a/src/engine/opts.rs +++ b/src/engine/opts.rs @@ -7,10 +7,16 @@ 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; +/// 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)] @@ -87,20 +93,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 +124,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 +149,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, + }, } } @@ -224,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(), @@ -254,17 +252,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 +273,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, MAX_HASH_SIZE, value)? as usize + } + "threads" => self.engine_opts.threads = parse_spin("threads", 0, 1024, value)? as usize, unknown => bail!("unknown option: {:?}", unknown), } @@ -328,30 +329,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/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..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; @@ -56,7 +55,17 @@ 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::>(); + + // 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; @@ -75,12 +84,18 @@ 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), + *engine_history.get(6).unwrap_or(&0), + ], }; - // 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(); + optlog!(search;warn;"history:{:?}", initial_options.history); // iterative deepening loop while !exit_condition() && target_depth < search_to() { diff --git a/src/engine/search/mod.rs b/src/engine/search/mod.rs index 8922fdc9..fde70ff1 100644 --- a/src/engine/search/mod.rs +++ b/src/engine/search/mod.rs @@ -1,8 +1,7 @@ //! The search module contains the search logic for the engine. mod main_search; -pub mod moveordering; -pub mod mv_heuristics; pub mod negamax; +pub mod quiescence; use std::fmt::Display; use std::ops::Neg; @@ -111,6 +110,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; 7], } /// wrapper around [`SEARCH_UNTIL`] diff --git a/src/engine/search/negamax.rs b/src/engine/search/negamax.rs index f3abf638..5588fd26 100644 --- a/src/engine/search/negamax.rs +++ b/src/engine/search/negamax.rs @@ -11,17 +11,17 @@ 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; 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; @@ -54,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 @@ -81,7 +77,7 @@ pub fn ng_bench( alpha, beta, Default::default(), - &opt, + &opt.engine_opts, &tt.get(), )) } @@ -93,9 +89,30 @@ 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:?}"); + + 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, + }; + } + // the initial move generator let mut base_gen = MoveGen::new_legal(&pos.chessboard); // slice for already generated moves. @@ -104,15 +121,12 @@ pub fn negamax( // we are mated! let out_of_moves = base_gen.len() == 0; - optlog!(search;trace;"ng: {pos}, td: {to_depth:?}, a: {alpha:?}, b: {beta:?}"); - /* 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 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(), @@ -123,19 +137,25 @@ pub fn negamax( 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 { + // 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 { + //|| to_depth == Depth::ZERO { let ev = evaluate(&pos, out_of_moves); optlog!(search;trace;"return eval: {:?}", ev); return SearchResult { @@ -161,11 +181,13 @@ pub fn negamax( // if theres 3 moves or less, search +1 level deeper }; + search_options.history.rotate_right(1); + search_options.history[0] = 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/quiescence.rs b/src/engine/search/quiescence.rs new file mode 100644 index 00000000..ef2c22a7 --- /dev/null +++ b/src/engine/search/quiescence.rs @@ -0,0 +1,95 @@ +//! 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::engine_opts::EngineOpts; +use crate::evaluation::evaluate; +use crate::move_generation::prio_iterator; +use crate::position::Position; +use crate::setup::depth::Depth; +use crate::setup::values::Value; + +/// make sure that we only statically evaluate after all capture moves have been +/// played (end of piece exchange) +/// +/// 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: &EngineOpts, +) -> 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, + } +} diff --git a/src/engine/search/tests/negatest.rs b/src/engine/search/tests/negatest.rs index f4564002..ff10592d 100644 --- a/src/engine/search/tests/negatest.rs +++ b/src/engine/search/tests/negatest.rs @@ -5,14 +5,12 @@ 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::opts::opts; -use crate::opts::setopts; +use crate::move_generation::prio_iterator; 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; @@ -24,11 +22,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 ); } @@ -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/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)); diff --git a/src/sandy/player/parse_move.rs b/src/sandy/player/parse_move.rs index f82e4ea1..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::search::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 { diff --git a/src/sandy/uci/mod.rs b/src/sandy/uci/mod.rs index c0634257..e15db2d5 100644 --- a/src/sandy/uci/mod.rs +++ b/src/sandy/uci/mod.rs @@ -71,8 +71,16 @@ 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)?; - println!("info string table resized to {entry_count} entries."); + engine.eng_opts = opt.engine_opts; + 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 {}.", 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()