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
69 changes: 36 additions & 33 deletions arche-core/src/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,12 +571,15 @@ mod tests {
assert_eq!(column("score"), position.score.to_string());
assert_eq!(column("refused"), position.refused_cutoffs.to_string());

// a ply deeper than the refusing run: at seven, once the passed pawn
// squares were scored, the test rank's tree met no tainted entry to
// trust
let trusting = SearchConfig::with_taint("trust").expect("trust is a policy");
let trusted = run_suite(&suite, 7, 1 << 20, trusting);
let trusted = run_suite(&suite, 8, 1 << 20, trusting);
assert!(
trusted
.to_string()
.starts_with("bench depth 7 hash 1MB positions 1 taint trust\n")
.starts_with("bench depth 8 hash 1MB positions 1 taint trust\n")
);
assert_eq!(trusted.positions[0].refused_cutoffs, 0);
assert!(trusted.positions[0].tainted_cutoffs > 0);
Expand Down Expand Up @@ -612,9 +615,9 @@ mod tests {
figures(skipping),
],
[
[177_161, 6_475, 22_519, 437, 0, 181, 0],
[12_948, 1_021, 3_399, 38, 15, 0, 0],
[14_088, 1_084, 3_717, 1, 0, 0, 8],
[182_505, 7_085, 22_788, 535, 0, 287, 0],
[11_884, 914, 3_056, 52, 24, 0, 0],
[15_165, 1_224, 3_823, 3, 0, 0, 39],
],
"nodes, cutoffs, stores, tainted, tcuts, refused, skipped"
);
Expand Down Expand Up @@ -830,24 +833,24 @@ mod tests {
assert_eq!(
counted,
vec![
("start", 495_700),
("italian", 1_121_074),
("ruy lopez", 366_742),
("kiwipete", 1_575_476),
("perft 4", 2_814_759),
("promotions", 1_983_365),
("middlegame", 532_079),
("sharp middlegame", 449_131),
("bratko kopec 1", 11_714),
("start", 564_019),
("italian", 1_137_689),
("ruy lopez", 378_968),
("kiwipete", 1_625_682),
("perft 4", 2_901_384),
("promotions", 1_643_811),
("middlegame", 494_644),
("sharp middlegame", 449_762),
("bratko kopec 1", 11_568),
("wac 4", 1_516),
("rook and pawns", 214_156),
("tarrasch", 238_448),
("lucena", 361_398),
("philidor", 357_659),
("minor endgame", 156_339),
("rook and pawns", 355_432),
("tarrasch", 136_354),
("lucena", 1_403_340),
("philidor", 237_927),
("minor endgame", 156_423),
("queen endgame", 337_681),
("king and pawn", 38_955),
("trebuchet", 32_344),
("king and pawn", 45_261),
("trebuchet", 35_531),
]
);
}
Expand Down Expand Up @@ -881,22 +884,22 @@ mod tests {
vec![
("start", 40_734),
("italian", 181_047),
("ruy lopez", 155_177),
("kiwipete", 220_649),
("perft 4", 172_939),
("ruy lopez", 155_167),
("kiwipete", 182_229),
("perft 4", 179_921),
("promotions", 101_321),
("middlegame", 159_802),
("sharp middlegame", 255_709),
("bratko kopec 1", 6_675),
("middlegame", 159_814),
("sharp middlegame", 255_669),
("bratko kopec 1", 6_654),
("wac 4", 675),
("rook and pawns", 21_701),
("tarrasch", 46_932),
("lucena", 28_842),
("philidor", 37_043),
("rook and pawns", 23_516),
("tarrasch", 32_961),
("lucena", 49_407),
("philidor", 38_260),
("minor endgame", 22_523),
("queen endgame", 132_956),
("king and pawn", 2_843),
("trebuchet", 1_162),
("king and pawn", 2_696),
("trebuchet", 1_007),
]
);
}
Expand Down
9 changes: 6 additions & 3 deletions arche-core/src/engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,11 +1021,14 @@ mod search {
// at a ridge of 3e-7, under which a queen and a rook promotion tie
// at depth six (573 each) and the two searches break the tie
// differently. A tie says nothing about the schedule, so the sharp
// middlegame stands in for it, and every position here answers with
// the same move both ways with the fitted table and the test rank
// middlegame stands in for it. The rook and pawns ending tied the
// same way at depth six once the passed pawn squares were scored, and
// the quiet middlegame stands in for that. Every position here
// answers with the same move both ways with the fitted table and the
// test rank
for fen in [
fens::KIWIPETE,
"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 10 10",
"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 1",
fens::SHARP_MIDDLEGAME,
] {
let mut direct = reference(Board::from_fen(fen).unwrap());
Expand Down
53 changes: 32 additions & 21 deletions arche-core/src/eval/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
//! One cache type, not one table: a term names its key, its fold and its
//! width, and [`super::Caches`] holds an instance of this per term.

/// One remembered score, under the whole key rather than a tag, since a
/// One remembered answer, under the whole key rather than a tag, since a
/// wrong hit would be a silently wrong evaluation. The whole key costs sixteen
/// bytes an entry against eight once the alignment is paid.
#[derive(Copy, Clone)]
struct Entry {
struct Entry<T> {
key: u64,
packed: i32,
value: T,
}

/// What one term has already worked out, `SLOTS` scores of it, in an array of
/// What one term has already worked out, `SLOTS` answers of it, in an array of
/// that length rather than a slice, so the slot's mask bounds its index and a
/// probe carries no bounds check.
///
Expand All @@ -26,25 +26,36 @@ struct Entry {
///
/// Owned by the searcher rather than by the board, because it is scratch and
/// not position.
pub(super) struct Cache<const SLOTS: usize> {
entries: Box<[Entry; SLOTS]>,
///
/// What it holds is a score by default; the passed pawn squares keep a set
/// of squares in one under the pawn key.
pub(super) struct Cache<const SLOTS: usize, T: Copy + Default = i32> {
entries: Box<[Entry<T>; SLOTS]>,
}

impl<const SLOTS: usize> Default for Cache<SLOTS> {
impl<const SLOTS: usize, T: Copy + Default> Default for Cache<SLOTS, T> {
fn default() -> Self {
// an empty entry is key zero holding zero. A pawnless board's pawn
// key is zero and its pawn structure scores zero, so it reads the
// entry correctly; any other key of zero is the same sixty four bit
// coincidence a wrong hit needs anywhere else in the table
let entries = vec![Entry { key: 0, packed: 0 }; Self::SLOTS].into_boxed_slice();
// key is zero and its pawn structure scores zero (and it has no
// passers), so it reads the entry correctly; any other key of zero is
// the same sixty four bit coincidence a wrong hit needs anywhere else
// in the table
let entries = vec![
Entry {
key: 0,
value: T::default()
};
Self::SLOTS
]
.into_boxed_slice();
Cache {
entries: entries.try_into().unwrap_or_else(|_| unreachable!()),
}
}
}

impl<const SLOTS: usize> Cache<SLOTS> {
/// How many scores the table holds. A power of two, so the index is a
impl<const SLOTS: usize, T: Copy + Default> Cache<SLOTS, T> {
/// How many answers the table holds. A power of two, so the index is a
/// mask rather than a remainder.
pub(super) const SLOTS: usize = {
assert!(SLOTS.is_power_of_two());
Expand All @@ -54,24 +65,24 @@ impl<const SLOTS: usize> Cache<SLOTS> {
/// What `key` stands for, read back where the table holds it and folded
/// where it does not.
#[inline]
pub(super) fn get(&mut self, key: u64, fold: impl FnOnce() -> i32) -> i32 {
pub(super) fn get(&mut self, key: u64, fold: impl FnOnce() -> T) -> T {
let slot = (key as usize) & (Self::SLOTS - 1);
let entry = &mut self.entries[slot];
if entry.key == key {
return entry.packed;
return entry.value;
}
let packed = fold();
*entry = Entry { key, packed };
packed
let value = fold();
*entry = Entry { key, value };
value
}

/// What the table holds under `key`, and nothing where the slot stands
/// for another key. For the tests in [`super`], which read the tables
/// rather than the score.
#[cfg(test)]
pub(super) fn stored(&self, key: u64) -> Option<i32> {
pub(super) fn stored(&self, key: u64) -> Option<T> {
let entry = &self.entries[(key as usize) & (Self::SLOTS - 1)];
(entry.key == key).then_some(entry.packed)
(entry.key == key).then_some(entry.value)
}
}

Expand Down Expand Up @@ -102,7 +113,7 @@ mod tests {
assert_eq!((shares as usize) & (Cache::<SLOTS>::SLOTS - 1), slot);

assert_eq!(cache.entries[slot].key, 0, "the slot starts empty");
assert_eq!(cache.entries[slot].packed, 0, "holding nothing");
assert_eq!(cache.entries[slot].value, 0, "holding nothing");
assert_eq!(cache.stored(key), None);
assert_eq!(cache.stored(0), Some(0), "and a key of zero reads it");

Expand Down
55 changes: 48 additions & 7 deletions arche-core/src/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod cache;
pub(crate) mod factors;
mod king_attack;
mod mobility;
mod passer_squares;
mod pawn_structure;
mod shelter;
mod tempo;
Expand Down Expand Up @@ -189,6 +190,12 @@ pub(crate) const TERMS: &[Term] = &[
weight: tempo::weight,
counts: tempo::counts,
},
Term {
name: "passer_squares",
width: passer_squares::COUNTS,
weight: passer_squares::weight,
counts: passer_squares::counts,
},
];

/// The widest term, which is how long a buffer the tuner's walk needs to ask
Expand Down Expand Up @@ -228,6 +235,9 @@ const fn widest() -> usize {
trait Memo {
/// The shelter and the pawn structure, added.
fn tables(&mut self, board: &Position) -> i32;
/// Both sides' passers on the squares [`passer_squares`] reads them on,
/// which the pawns alone decide.
fn passers(&mut self, board: &Position) -> u64;
}

/// The memo that remembers nothing, which is what [`eval`] hands the sum.
Expand All @@ -238,6 +248,11 @@ impl Memo for NoMemo {
fn tables(&mut self, board: &Position) -> i32 {
shelter::fold(board) + pawn_structure::fold(board)
}

#[inline]
fn passers(&mut self, board: &Position) -> u64 {
passer_squares::passers(board)
}
}

/// What a searcher carries so that the two remembered terms are not computed
Expand All @@ -249,16 +264,24 @@ impl Memo for NoMemo {
/// The pawn table stays behind it: a key of the shelter's alone would
/// recompute the pawn structure on every king move, the half of the
/// shelter's key that term does not need. Measured on the fitted build.
///
/// The passed pawn squares read the pieces and are not remembered, but which
/// pawns they read is the pawns' alone, so that set has a table of its own
/// under the pawn key.
#[derive(Default)]
pub(crate) struct Caches {
shelter: ShelterCache,
pawns: PawnCache,
passers: PasserCache,
}

/// The shelter's table, as wide as that term measured it wants.
type ShelterCache = Cache<{ 1 << shelter::CACHE_BITS }>;
/// The pawn structure's, on its own key and its own measurement.
type PawnCache = Cache<{ 1 << pawn_structure::CACHE_BITS }>;
/// The passers the passed pawn squares read, under the same key at the same
/// width as the pawn structure's.
type PasserCache = Cache<{ 1 << pawn_structure::CACHE_BITS }, u64>;

impl Memo for Caches {
#[inline]
Expand All @@ -268,6 +291,12 @@ impl Memo for Caches {
shelter::fold(board) + pawns.get(board.pawn_key, || pawn_structure::fold(board))
})
}

#[inline]
fn passers(&mut self, board: &Position) -> u64 {
self.passers
.get(board.pawn_key, || passer_squares::passers(board))
}
}

/// The score of the position from the side to move's point of view, with the
Expand All @@ -294,7 +323,13 @@ fn sum(board: &Position, memo: &mut impl Memo) -> Score {
let tables = memo.tables(board);
let walk =
|color| attack_score::<{ mobility::SCORED_KINDS }, { king_attack::SCORED }>(board, color);
let leaf = walk(Color::White) - walk(Color::Black) + tables + tempo::fold(board);
// read at the leaf: it reads the pieces, which no memo here is keyed on
let passers = if passer_squares::SCORED {
passer_squares::fold(board, memo.passers(board))
} else {
0
};
let leaf = walk(Color::White) - walk(Color::Black) + tables + tempo::fold(board) + passers;
board.eval.score(board.active_color, leaf)
}

Expand Down Expand Up @@ -542,8 +577,8 @@ pub(crate) fn suite_fens() -> Vec<String> {
mod evaluate {
use super::{
Board, Caches, Memo, PawnCache, ShelterCache, TERMS, TOTAL_PHASE, eval, eval_cached,
factors, king_attack, mobility, pawn_structure, pieces_of, shelter, suite_fens, tempo,
weigh,
factors, king_attack, mobility, passer_squares, pawn_structure, pieces_of, shelter,
suite_fens, tempo, weigh,
};
use crate::board::fens;
use crate::misc::{Color, File, coordinate_to_index};
Expand Down Expand Up @@ -1040,7 +1075,7 @@ mod evaluate {
}
}

/// The table names the five leaf terms the sum adds. The sum is hand
/// The table names the six leaf terms the sum adds. The sum is hand
/// written rather than a walk over the table, so this is the one place
/// the two lists are held against each other; a priced term in the table
/// and not the sum would fail the tuner's identity, and this says which
Expand All @@ -1055,11 +1090,12 @@ mod evaluate {
"shelter",
"pawn_structure",
"king_attack",
"tempo"
"tempo",
"passer_squares"
]
);
// two queens and a rook against none, a king in each corner and pawns
// of both colours on six files, so that no one of the five folds to
// of both colours on six files, so that no one of the six folds to
// nothing. The queen on a4 bears on d7 and e8 of the black king's
// ring
let board = Board::from_fen("3k4/P4p2/8/3P2p1/Q2P4/PP2p2p/1P6/1Q4KR w - - 0 1").unwrap();
Expand All @@ -1069,14 +1105,19 @@ mod evaluate {
("pawn structure", pawn_structure::fold(&board)),
("king attack", king_attack::fold(&board)),
("tempo", tempo::fold(&board)),
(
"passer squares",
passer_squares::fold(&board, passer_squares::passers(&board)),
),
] {
assert_ne!(term, 0, "{} is level here, so it says nothing", name);
}
let leaf = mobility::fold(&board)
+ shelter::fold(&board)
+ pawn_structure::fold(&board)
+ king_attack::fold(&board)
+ tempo::fold(&board);
+ tempo::fold(&board)
+ passer_squares::fold(&board, passer_squares::passers(&board));
assert_eq!(eval(&board), board.eval.score(board.active_color, leaf));
}

Expand Down
Loading
Loading