Skip to content
Merged
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
33 changes: 28 additions & 5 deletions arche-core/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1405,7 +1405,9 @@ impl AlphaBeta {

/// A node decision just taken, the margin's or the pass's, offered to
/// the forced decision instrument. True when it is the decision being
/// inverted, which the caller then does not take.
/// inverted, which the caller then does not take. `gap` is how far the
/// decision's bound cleared beta: the margin's floor or the pass's
/// score, less beta.
// cold and out of line behind a bare is_some, for `sample`'s reason
#[cold]
#[inline(never)]
Expand All @@ -1416,6 +1418,7 @@ impl AlphaBeta {
eval: Score,
alpha: Score,
beta: Score,
gap: i32,
) -> bool {
let address = forced::Address {
kind,
Expand All @@ -1436,6 +1439,7 @@ impl AlphaBeta {
features: None,
eval_beta: i32::from(eval) - i32::from(beta),
alpha_gap: i32::from(alpha) - i32::from(eval),
gap: Some(gap),
fen: board.to_fen(),
});
false
Expand Down Expand Up @@ -1495,6 +1499,7 @@ impl AlphaBeta {
features: Some(features),
eval_beta: eval_beta as i32,
alpha_gap: alpha_gap as i32,
gap: None,
fen: board.to_fen(),
}
});
Expand All @@ -1505,7 +1510,8 @@ impl AlphaBeta {
/// forced decision instrument before it answers for its move. True when
/// it is the decision being inverted. The board is the position the
/// move left, which is the address, and the node's own evaluation is
/// read by stepping the move back, for kept events alone.
/// read by stepping the move back, for kept events alone. `scout` is
/// the scout's fail soft score, at or under alpha.
#[cold]
#[inline(never)]
fn forced_scout(
Expand All @@ -1514,6 +1520,7 @@ impl AlphaBeta {
depth: u8,
alpha: Score,
beta: Score,
scout: Score,
) -> bool {
let address = forced::Address {
kind: forced::Kind::TrustedScout,
Expand Down Expand Up @@ -1546,6 +1553,7 @@ impl AlphaBeta {
features: Some(staged.features),
eval_beta: eval_beta as i32,
alpha_gap: alpha_gap as i32,
gap: Some(i32::from(alpha) - i32::from(scout)),
fen,
}
});
Expand Down Expand Up @@ -2236,7 +2244,14 @@ impl AlphaBeta {
}
if floor >= beta {
if self.forced.is_some()
&& self.forced_node(forced::Kind::ReverseFutility, depth, eval, alpha, beta)
&& self.forced_node(
forced::Kind::ReverseFutility,
depth,
eval,
alpha,
beta,
i32::from(floor) - i32::from(beta),
)
{
margin_inverted = true;
} else {
Expand Down Expand Up @@ -2265,7 +2280,14 @@ impl AlphaBeta {
let value = -result?;
if value.score >= beta
&& !(self.forced.is_some()
&& self.forced_node(forced::Kind::NullMove, depth, eval, alpha, beta))
&& self.forced_node(
forced::Kind::NullMove,
depth,
eval,
alpha,
beta,
i32::from(value.score) - i32::from(beta),
))
{
// a mate found through a pass is not a mate, since the pass
// is not a legal move, so the score is held under the window
Expand Down Expand Up @@ -2378,7 +2400,8 @@ impl AlphaBeta {
);
}
if scout.score <= alpha
&& !(self.forced.is_some() && self.forced_scout(staged, depth, alpha, beta))
&& !(self.forced.is_some()
&& self.forced_scout(staged, depth, alpha, beta, scout.score))
{
return Ok(scout);
}
Expand Down
13 changes: 10 additions & 3 deletions arche-core/src/forced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ pub struct Event {
pub(crate) features: Option<Features>,
pub eval_beta: i32,
pub alpha_gap: i32,
/// How far the decision's own bound cleared its window, in the
/// direction that took the decision: for a trusted scout, the alpha it
/// was read against less its fail soft score; for the null move, the
/// pass's score less beta; for reverse futility, the margin's floor
/// less beta. Zero at the bound. None on a skip, which searches nothing.
pub gap: Option<i32>,
/// The deciding node's position.
pub fen: String,
}
Expand Down Expand Up @@ -396,11 +402,11 @@ pub fn run(
/// and what the default answered at each root.
///
/// A row is `kind depth index searched generated history history_max
/// killer tt eval_beta alpha_gap answered visits root best_on
/// killer tt eval_beta alpha_gap gap answered visits root best_on
/// best_forced score_on score_forced nodes_on nodes_forced fen`,
/// whitespace separated with the deciding node's fen last. `root` is the
/// root's place in the suite, which its line names. A move decision's own
/// columns print `-` on a node decision.
/// columns print `-` on a node decision, and `gap` prints `-` on a skip.
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
Expand Down Expand Up @@ -435,12 +441,13 @@ impl fmt::Display for Report {
};
writeln!(
f,
"{} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
"{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
e.address.kind.word(),
e.address.depth,
features,
e.eval_beta,
e.alpha_gap,
e.gap.map_or_else(|| "-".to_string(), |gap| gap.to_string()),
row.answered.map_or("-", Answered::word),
row.visits,
e.root,
Expand Down
10 changes: 7 additions & 3 deletions docs/INSTRUMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,13 @@ quiets at once never meets the skips in it. The tests hold an armed search's
move, score and node count to an unarmed one's.

Each row is `kind depth index searched generated history history_max killer
tt eval_beta alpha_gap answered visits root best_on best_forced score_on
score_forced nodes_on nodes_forced fen`. The move columns are the ledger's,
read at the first visit, and print `-` on a node decision. Every skip at
tt eval_beta alpha_gap gap answered visits root best_on best_forced
score_on score_forced nodes_on nodes_forced fen`. The move columns are the
ledger's, read at the first visit, and print `-` on a node decision. `gap`
is how far the decision's own bound cleared its window, zero at the bound:
for a trusted scout the alpha less its fail soft score, for the null move
the pass's score less beta, and for reverse futility the margin's floor
less beta. A skip searched nothing and prints `-`. Every skip at
depth four and up has an `eval_beta` at or under minus the skip's margin by
construction. `answered` is, for a node decision, what answered the node once it
was inverted. `visits` is how often the forced search met the address, and a
Expand Down
14 changes: 10 additions & 4 deletions tests/forced_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@ fn the_forced_argument_prints_a_header_rows_and_a_line_a_kind_and_a_root() {
assert!(printed.events() > 0, "header: {}", printed.header);
for row in &printed.rows {
let words: Vec<&str> = row.split(' ').collect();
// twenty columns before a fen of six fields
assert_eq!(words.len(), 26, "row: {row}");
// twenty one columns before a fen of six fields
assert_eq!(words.len(), 27, "row: {row}");
assert!(
["reverse_futility", "null_move", "skip", "trusted_scout"].contains(&words[0]),
"row: {row}"
);
// depth, visits, root, the two scores and the two node counts
for at in [1, 12, 13, 16, 17, 18, 19] {
for at in [1, 13, 14, 17, 18, 19, 20] {
assert!(words[at].parse::<i64>().is_ok(), "field {at} of {row}");
}
// every decision but a skip cleared its bound, by zero or more
if words[0] == "skip" {
assert_eq!(words[11], "-", "row: {row}");
} else {
assert!(words[11].parse::<u32>().is_ok(), "row: {row}");
}
// a kept decision the forced search never met is a failed row
assert!(words[12] != "0", "row: {row}");
assert!(words[13] != "0", "row: {row}");
}
let kinds = printed
.summary
Expand Down
Loading