Compare commits
No commits in common. "1d651de4a0001a31195e51d65380abc28328f09b" and "86e5780f267150365ab0fea4f4bef12ca5f4adfb" have entirely different histories.
1d651de4a0
...
86e5780f26
@ -1,82 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Runs a fast-chess (https://github.com/Disservin/fastchess) tournament based
|
|
||||||
# on two branches of the chess_inator
|
|
||||||
# (https://github.com/dogeystamp/chess_inator) engine.
|
|
||||||
#
|
|
||||||
# Example usage:
|
|
||||||
#
|
|
||||||
# cd chess_tournaments
|
|
||||||
# fast-chess-branch.sh quiescence no-quiescence -openings file=8moves_v3.pgn format=pgn order=random -each tc=300+0.1 -rounds 12 -repeat -concurrency 8 -recover -sprt elo0=0 elo1=10 alpha=0.05 beta=0.05
|
|
||||||
#
|
|
||||||
# Do not use `main` as a branch, or any other branch already checked out in
|
|
||||||
# another directory. You need to be in a chess_inator Git repository to run
|
|
||||||
# this script. Ensure that the repository you're in is a throw-away worktree.
|
|
||||||
# Create one using
|
|
||||||
#
|
|
||||||
# git worktree add ../chess_tournaments
|
|
||||||
#
|
|
||||||
# inside the original chess_inator repo.
|
|
||||||
# Also, get an opening book from Stockfish's books:
|
|
||||||
#
|
|
||||||
# curl -O https://github.com/official-stockfish/books/raw/refs/heads/master/8moves_v3.pgn.zip
|
|
||||||
#
|
|
||||||
# The sprt mode is a statistical hypothesis testing mode that will tell you how
|
|
||||||
# probably the first branch is better than the second branch. The Elo ratings
|
|
||||||
# given are the "indifference zone" where the result is acceptable. To check
|
|
||||||
# that the engine hasn't had a regression, set them to [-10, 0]. To check for
|
|
||||||
# an improvement, use [0, 10]. Alpha and beta are probabilities for statistical
|
|
||||||
# errors. The tournament automatically ends when a statistically significant
|
|
||||||
# result is obtained.
|
|
||||||
#
|
|
||||||
# By default, a PGN file will be exported with the games played, and the
|
|
||||||
# fast-chess SPRT output will be appended. This comment may interfere with
|
|
||||||
# importing the PGN. But Lichess will ignore it, so it's probably fine.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BRANCH1="$1"
|
|
||||||
BRANCH2="$2"
|
|
||||||
|
|
||||||
# if this line fails it's because you don't have enough arguments
|
|
||||||
shift 2
|
|
||||||
|
|
||||||
COMM1=$(git rev-parse --short "$BRANCH1")
|
|
||||||
COMM2=$(git rev-parse --short "$BRANCH2")
|
|
||||||
|
|
||||||
mkdir -p games
|
|
||||||
|
|
||||||
PGN=games/"$BRANCH1"__"$BRANCH2".pgn
|
|
||||||
|
|
||||||
rm -f engine1 engine2
|
|
||||||
if [ -f "$PGN" ]; then
|
|
||||||
rm -i "$PGN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
git switch "$BRANCH1"
|
|
||||||
cargo build --release
|
|
||||||
cp target/release/chess_inator engine1
|
|
||||||
|
|
||||||
git switch "$BRANCH2"
|
|
||||||
cargo build --release
|
|
||||||
cp target/release/chess_inator engine2
|
|
||||||
|
|
||||||
OUTPUT=$(mktemp)
|
|
||||||
|
|
||||||
fastchess \
|
|
||||||
-engine cmd=engine1 name="c_i $BRANCH1 ($COMM1)" \
|
|
||||||
-engine cmd=engine2 name="c_i $BRANCH2 ($COMM2)" \
|
|
||||||
-pgnout file="$PGN" \
|
|
||||||
timeleft=true \
|
|
||||||
$@ \
|
|
||||||
2>&1 | tee -a "$OUTPUT"
|
|
||||||
|
|
||||||
printf "\n{" >> "$PGN"
|
|
||||||
|
|
||||||
# match between ------- markers in fastchess output, strip newline and then output to PGN
|
|
||||||
awk '/-{50}/{f+=1; print; next} f%2' "$OUTPUT" \
|
|
||||||
| head -c -1 \
|
|
||||||
>> "$PGN"
|
|
||||||
|
|
||||||
printf "}" >> "$PGN"
|
|
||||||
|
|
||||||
rm "$OUTPUT"
|
|
@ -1,39 +0,0 @@
|
|||||||
# /usr/bin/env python
|
|
||||||
|
|
||||||
"""
|
|
||||||
Converts PGN files from fast-chess's `tl` time left notation to the standard `%clk` clock time notation.
|
|
||||||
|
|
||||||
May be buggy; other comments may break this script.
|
|
||||||
Takes stdin and converts to stdout.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
|
|
||||||
pgn_value = sys.stdin.read()
|
|
||||||
|
|
||||||
|
|
||||||
def convert(m: re.Match[str]) -> str:
|
|
||||||
seconds_total = float(m.group(1))
|
|
||||||
mins, secs = divmod(seconds_total, 60)
|
|
||||||
hrs, mins = divmod(mins, 60)
|
|
||||||
|
|
||||||
secs = round(secs, 4)
|
|
||||||
mins = round(mins)
|
|
||||||
hrs = round(hrs)
|
|
||||||
|
|
||||||
return f"{{ [%clk {hrs}:{mins:02}:{secs}] }}"
|
|
||||||
|
|
||||||
pgn_value = re.sub(
|
|
||||||
pattern=r"{book}",
|
|
||||||
repl="",
|
|
||||||
string=pgn_value,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
re.sub(
|
|
||||||
pattern=r"{.*?tl=(.*?)s.*?}",
|
|
||||||
repl=convert,
|
|
||||||
string=pgn_value,
|
|
||||||
)
|
|
||||||
)
|
|
@ -466,9 +466,6 @@ pub struct Board {
|
|||||||
|
|
||||||
/// Hash state to incrementally update.
|
/// Hash state to incrementally update.
|
||||||
zobrist: Zobrist,
|
zobrist: Zobrist,
|
||||||
|
|
||||||
/// Last captured square
|
|
||||||
recap_sq: Option<Square>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
@ -544,7 +541,6 @@ impl Board {
|
|||||||
castle: CastleRights(self.castle.0),
|
castle: CastleRights(self.castle.0),
|
||||||
eval: Default::default(),
|
eval: Default::default(),
|
||||||
zobrist: Zobrist::default(),
|
zobrist: Zobrist::default(),
|
||||||
recap_sq: self.recap_sq.map(|sq| sq.mirror_vert()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
new_board.castle.0.reverse();
|
new_board.castle.0.reverse();
|
||||||
|
149
src/search.rs
149
src/search.rs
@ -110,8 +110,6 @@ pub struct SearchConfig {
|
|||||||
pub alpha_beta_on: bool,
|
pub alpha_beta_on: bool,
|
||||||
/// Limit regular search depth
|
/// Limit regular search depth
|
||||||
pub depth: usize,
|
pub depth: usize,
|
||||||
/// Limit quiescence search depth
|
|
||||||
pub qdepth: usize,
|
|
||||||
/// Enable transposition table.
|
/// Enable transposition table.
|
||||||
pub enable_trans_table: bool,
|
pub enable_trans_table: bool,
|
||||||
/// Transposition table size (2^n where this is n)
|
/// Transposition table size (2^n where this is n)
|
||||||
@ -124,7 +122,6 @@ impl Default for SearchConfig {
|
|||||||
alpha_beta_on: true,
|
alpha_beta_on: true,
|
||||||
// try to make this even to be more conservative and avoid horizon problem
|
// try to make this even to be more conservative and avoid horizon problem
|
||||||
depth: 10,
|
depth: 10,
|
||||||
qdepth: 1,
|
|
||||||
enable_trans_table: true,
|
enable_trans_table: true,
|
||||||
transposition_size: 24,
|
transposition_size: 24,
|
||||||
}
|
}
|
||||||
@ -158,33 +155,26 @@ fn move_priority(board: &mut Board, mv: &Move, state: &mut EngineState) -> EvalI
|
|||||||
eval
|
eval
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State specifically for a minmax call.
|
|
||||||
struct MinmaxState {
|
|
||||||
/// how many plies left to search in this call
|
|
||||||
depth: usize,
|
|
||||||
/// best score (absolute, from current player perspective) guaranteed for current player.
|
|
||||||
alpha: Option<EvalInt>,
|
|
||||||
/// best score (absolute, from current player perspective) guaranteed for other player.
|
|
||||||
beta: Option<EvalInt>,
|
|
||||||
/// quiescence search flag
|
|
||||||
quiesce: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search the game tree to find the absolute (positive good) move and corresponding eval for the
|
/// Search the game tree to find the absolute (positive good) move and corresponding eval for the
|
||||||
/// current player.
|
/// current player.
|
||||||
///
|
///
|
||||||
/// This also integrates quiescence search, which looks for a calm (quiescent) position where
|
|
||||||
/// there are no recaptures.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * board: board position to analyze.
|
/// * board: board position to analyze.
|
||||||
/// * depth: how deep to analyze the game tree.
|
/// * depth: how deep to analyze the game tree.
|
||||||
|
/// * alpha: best score (absolute, from current player perspective) guaranteed for current player.
|
||||||
|
/// * beta: best score (absolute, from current player perspective) guaranteed for other player.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// The best line (in reverse move order), and its corresponding absolute eval for the current player.
|
/// The best line (in reverse move order), and its corresponding absolute eval for the current player.
|
||||||
fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<Move>, SearchEval) {
|
fn minmax(
|
||||||
|
board: &mut Board,
|
||||||
|
state: &mut EngineState,
|
||||||
|
depth: usize,
|
||||||
|
alpha: Option<EvalInt>,
|
||||||
|
beta: Option<EvalInt>,
|
||||||
|
) -> (Vec<Move>, SearchEval) {
|
||||||
// these operations are relatively expensive, so only run them occasionally
|
// these operations are relatively expensive, so only run them occasionally
|
||||||
if state.node_count % (1 << 16) == 0 {
|
if state.node_count % (1 << 16) == 0 {
|
||||||
// respect the hard stop if given
|
// respect the hard stop if given
|
||||||
@ -209,30 +199,15 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mm.depth == 0 {
|
|
||||||
if mm.quiesce || board.recap_sq.is_none() {
|
|
||||||
// if we're done with quiescence, static eval.
|
|
||||||
// if there is no capture, skip straight to static eval.
|
|
||||||
let eval = board.eval() * EvalInt::from(board.turn.sign());
|
|
||||||
return (Vec::new(), SearchEval::Exact(eval));
|
|
||||||
} else {
|
|
||||||
return minmax(
|
|
||||||
board,
|
|
||||||
state,
|
|
||||||
MinmaxState {
|
|
||||||
depth: state.config.qdepth,
|
|
||||||
alpha: mm.alpha,
|
|
||||||
beta: mm.beta,
|
|
||||||
quiesce: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// default to worst, then gradually improve
|
// default to worst, then gradually improve
|
||||||
let mut alpha = mm.alpha.unwrap_or(EVAL_WORST);
|
let mut alpha = alpha.unwrap_or(EVAL_WORST);
|
||||||
// our best is their worst
|
// our best is their worst
|
||||||
let beta = mm.beta.unwrap_or(EVAL_BEST);
|
let beta = beta.unwrap_or(EVAL_BEST);
|
||||||
|
|
||||||
|
if depth == 0 {
|
||||||
|
let eval = board.eval() * EvalInt::from(board.turn.sign());
|
||||||
|
return (Vec::new(), SearchEval::Exact(eval));
|
||||||
|
}
|
||||||
|
|
||||||
let mut mvs: Vec<_> = board
|
let mut mvs: Vec<_> = board
|
||||||
.gen_moves()
|
.gen_moves()
|
||||||
@ -245,7 +220,7 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
|
|||||||
// get transposition table entry
|
// get transposition table entry
|
||||||
if state.config.enable_trans_table {
|
if state.config.enable_trans_table {
|
||||||
if let Some(entry) = &state.cache[board.zobrist] {
|
if let Some(entry) = &state.cache[board.zobrist] {
|
||||||
if entry.is_qsearch == mm.quiesce && entry.depth >= mm.depth {
|
if entry.depth >= depth {
|
||||||
if let SearchEval::Exact(_) | SearchEval::Upper(_) = entry.eval {
|
if let SearchEval::Exact(_) | SearchEval::Upper(_) = entry.eval {
|
||||||
// no point looking for a better move
|
// no point looking for a better move
|
||||||
return (vec![entry.best_move], entry.eval);
|
return (vec![entry.best_move], entry.eval);
|
||||||
@ -262,48 +237,18 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
|
|||||||
let mut best_move: Option<Move> = None;
|
let mut best_move: Option<Move> = None;
|
||||||
let mut best_continuation: Vec<Move> = Vec::new();
|
let mut best_continuation: Vec<Move> = Vec::new();
|
||||||
|
|
||||||
let n_non_qmoves = mvs.len();
|
if mvs.is_empty() {
|
||||||
|
if board.is_check(board.turn) {
|
||||||
// determine moves that are allowed in quiescence
|
|
||||||
if mm.quiesce {
|
|
||||||
mvs.retain(|(_priority, mv): &(EvalInt, Move)| -> bool {
|
|
||||||
if let Some(recap_sq) = board.recap_sq {
|
|
||||||
if mv.dest == recap_sq {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if n_non_qmoves == 0 {
|
|
||||||
let is_in_check = board.is_check(board.turn);
|
|
||||||
|
|
||||||
if is_in_check {
|
|
||||||
return (Vec::new(), SearchEval::Checkmate(-1));
|
return (Vec::new(), SearchEval::Checkmate(-1));
|
||||||
} else {
|
} else {
|
||||||
// stalemate
|
// stalemate
|
||||||
return (Vec::new(), SearchEval::Exact(0));
|
return (Vec::new(), SearchEval::Exact(0));
|
||||||
}
|
}
|
||||||
} else if mvs.is_empty() {
|
|
||||||
// pruned all the moves due to quiescence
|
|
||||||
let eval = board.eval() * EvalInt::from(board.turn.sign());
|
|
||||||
return (Vec::new(), SearchEval::Exact(eval));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (_priority, mv) in mvs {
|
for (_priority, mv) in mvs {
|
||||||
let anti_mv = mv.make(board);
|
let anti_mv = mv.make(board);
|
||||||
let (continuation, score) = minmax(
|
let (continuation, score) = minmax(board, state, depth - 1, Some(-beta), Some(-alpha));
|
||||||
board,
|
|
||||||
state,
|
|
||||||
MinmaxState {
|
|
||||||
depth: mm.depth - 1,
|
|
||||||
alpha: Some(-beta),
|
|
||||||
beta: Some(-alpha),
|
|
||||||
quiesce: mm.quiesce,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// propagate hard stops
|
// propagate hard stops
|
||||||
if matches!(score, SearchEval::Stopped) {
|
if matches!(score, SearchEval::Stopped) {
|
||||||
@ -338,8 +283,7 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
|
|||||||
state.cache[board.zobrist] = Some(TranspositionEntry {
|
state.cache[board.zobrist] = Some(TranspositionEntry {
|
||||||
best_move,
|
best_move,
|
||||||
eval: abs_best,
|
eval: abs_best,
|
||||||
depth: mm.depth,
|
depth,
|
||||||
is_qsearch: mm.quiesce,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,49 +300,48 @@ pub struct TranspositionEntry {
|
|||||||
eval: SearchEval,
|
eval: SearchEval,
|
||||||
/// depth of this entry
|
/// depth of this entry
|
||||||
depth: usize,
|
depth: usize,
|
||||||
/// is this score within the context of quiescence
|
|
||||||
is_qsearch: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type TranspositionTable = ZobristTable<TranspositionEntry>;
|
pub type TranspositionTable = ZobristTable<TranspositionEntry>;
|
||||||
|
|
||||||
/// Iteratively deepen search until it is stopped.
|
/// Iteratively deepen search until it is stopped.
|
||||||
fn iter_deep(board: &mut Board, state: &mut EngineState) -> (Vec<Move>, SearchEval) {
|
fn iter_deep(board: &mut Board, state: &mut EngineState) -> (Vec<Move>, SearchEval) {
|
||||||
let (mut prev_line, mut prev_eval) = minmax(
|
// always preserve two lines (1 is most recent)
|
||||||
board,
|
let (mut line1, mut eval1) = minmax(board, state, 1, None, None);
|
||||||
state,
|
let (mut line2, mut eval2) = (line1.clone(), eval1);
|
||||||
MinmaxState {
|
|
||||||
depth: 1,
|
|
||||||
alpha: None,
|
|
||||||
beta: None,
|
|
||||||
quiesce: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
for depth in 2..=state.config.depth {
|
for depth in 2..=state.config.depth {
|
||||||
let (line, eval) = minmax(
|
let (line, eval) = minmax(board, state, depth, None, None);
|
||||||
board,
|
|
||||||
state,
|
let mut have_to_ret = false;
|
||||||
MinmaxState {
|
// depth of the line we're about to return.
|
||||||
depth,
|
// our knock-off "quiescence" is skeptical of odd depths, so we need to know this.
|
||||||
alpha: None,
|
let mut ret_depth = depth;
|
||||||
beta: None,
|
|
||||||
quiesce: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if matches!(eval, SearchEval::Stopped) {
|
if matches!(eval, SearchEval::Stopped) {
|
||||||
return (prev_line, prev_eval);
|
ret_depth -= 1;
|
||||||
|
have_to_ret = true;
|
||||||
} else {
|
} else {
|
||||||
|
(line2, eval2) = (line1, eval1);
|
||||||
|
(line1, eval1) = (line, eval);
|
||||||
if let Some(soft_lim) = state.time_lims.soft {
|
if let Some(soft_lim) = state.time_lims.soft {
|
||||||
if Instant::now() > soft_lim {
|
if Instant::now() > soft_lim {
|
||||||
return (line, eval);
|
have_to_ret = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(prev_line, prev_eval) = (line, eval);
|
}
|
||||||
|
|
||||||
|
if have_to_ret {
|
||||||
|
if ret_depth & 1 == 1 && (EvalInt::from(eval1) - EvalInt::from(eval2) > 300) {
|
||||||
|
// be skeptical if we move last and we suddenly earn a lot of
|
||||||
|
// centipawns. this may be a sign of horizon problem
|
||||||
|
return (line2, eval2);
|
||||||
|
} else {
|
||||||
|
return (line1, eval1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(prev_line, prev_eval)
|
(line1, eval1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deadlines for the engine to think of a move.
|
/// Deadlines for the engine to think of a move.
|
||||||
|
Loading…
Reference in New Issue
Block a user