Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "chesseng"
version = "0.6.2"
version = "0.6.3"
edition = "2024"
authors = ["Andreas Tsatsanis"]

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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<ChessMove>` 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
Expand Down
16 changes: 16 additions & 0 deletions research/games/quiescence_release.tournament
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/benches/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/benches/eval_grind.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/benches/iterative_deepening.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/benches/move_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/benches/move_gen_grind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/benches/ngm_full.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fn negamax_benches(c: &mut Criterion) {
.map(Position::from)
.collect::<Vec<Position>>();

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
Expand Down
20 changes: 20 additions & 0 deletions src/engine/engine_opts.rs
Original file line number Diff line number Diff line change
@@ -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,
}
14 changes: 13 additions & 1 deletion src/engine/evaluation/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 10 additions & 9 deletions src/engine/evaluation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
};
}

Expand Down
24 changes: 24 additions & 0 deletions src/engine/evaluation/position.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
28 changes: 6 additions & 22 deletions src/engine/evaluation/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
8 changes: 4 additions & 4 deletions src/engine/evaluation/tests/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ 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]
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]
Expand All @@ -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}");
}
8 changes: 7 additions & 1 deletion src/engine/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

pub mod book;
pub mod debug;
pub mod engine_opts;
pub mod evaluation;
pub mod move_generation;
pub mod opts;
Expand All @@ -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;
Expand All @@ -47,6 +50,8 @@ pub struct Engine {
pub table: TT,
/// recently played positions. used to detect 3-fold repetition.
pub history: VecDeque<Position>,
/// this instance's options
pub eng_opts: EngineOpts,
}

impl Engine {
Expand All @@ -58,6 +63,7 @@ impl Engine {
board: Default::default(),
table: TT::new(),
history: VecDeque::new(),
eng_opts: opts()?.engine_opts,
})
}

Expand All @@ -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`]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ pub fn mvv_lva_score(b: &Board, mv: &ChessMove) -> Value {
_ => Value::ZERO,
}
}

#[cfg(test)]
#[path = "./tests/heuristics.rs"]
mod tests;
Loading