#![allow(missing_docs)]
use std::path::Path;
use std::time::Instant;
use cbvault_chess::decode::{GameRef, MoveSink, NULL_MOVE, walk_from};
use cbvault_chess::start::start_board;
use gigachess::bitboard::{KING_ATT, KNIGHT_ATT};
use gigachess::san::{San, move_to_san_body};
use gigachess::types::Piece;
use gigachess::{Board, Color, Move, Role, Square, attacks};
fn san_body_per_candidate(board: &Board, mv: Move) -> Option<San> {
let from = mv.from();
let to = mv.to();
let piece = board.piece_at(from)?;
let mut out = San::new();
let is_castle = piece.role == Role::King && board.piece_at(to) == Some(Piece::new(piece.color, Role::Rook));
if is_castle {
out.push_str(if to.file() > from.file() { "O-O" } else { "O-O-O" });
} else {
let is_capture = board.piece_at(to).is_some()
|| (piece.role == Role::Pawn && board.en_passant() == Some(to) && from.file() != to.file());
if piece.role != Role::Pawn {
out.push(piece.role.char_upper());
let occ = board.occupied();
let att = match piece.role {
Role::Knight => KNIGHT_ATT[to.index()],
Role::Bishop => attacks::bishop_attacks(to.0, occ),
Role::Rook => attacks::rook_attacks(to.0, occ),
Role::Queen => attacks::queen_attacks(to.0, occ),
Role::King => KING_ATT[to.index()],
_ => 0,
};
let others = att & board.piece_bb(piece.color, piece.role) & !(1u64 << from.0);
if others != 0 {
let mut same_file = false;
let mut same_rank = false;
let mut any = false;
for cand in 0..64u8 {
if others & (1u64 << cand) == 0 {
continue;
}
let mut tmp = *board;
let cm = Move::new(Square::new(cand), to, mv.promotion());
let mover = board.turn();
if let Ok(undo) = tmp.play(cm)
&& tmp.attackers_to(tmp.king_square(mover).0, mover.other(), tmp.occupied()) == 0
{
tmp.unmake_move(cm, undo);
any = true;
let cs = Square::new(cand);
same_file |= cs.file() == from.file();
same_rank |= cs.rank() == from.rank();
}
}
if any {
if !same_file {
out.push((b'a' + from.file()) as char);
} else if !same_rank {
out.push((b'1' + from.rank()) as char);
} else {
out.push((b'a' + from.file()) as char);
out.push((b'1' + from.rank()) as char);
}
}
}
} else if is_capture {
out.push((b'a' + from.file()) as char);
}
if is_capture {
out.push('x');
}
let [tf, tr] = to.to_alg();
out.push(tf as char);
out.push(tr as char);
if let Some(p) = mv.promotion() {
out.push('=');
out.push(p.char_upper());
}
}
Some(out)
}
#[derive(Default)]
struct Stats {
moves: u64,
prefilter_hits: u64,
disambiguated: u64,
mismatches: u64,
}
#[derive(Default)]
struct Sink {
gigachess: bool,
per_candidate: bool,
sink_san: bool,
mate: bool,
suffix: bool,
stats: Stats,
examples: Vec<String>,
}
impl MoveSink for Sink {
fn play(&mut self, before: &Board, mv: u16, _main: bool) {
if self.mate {
return;
}
self.stats.moves += 1;
if mv == NULL_MOVE {
return;
}
let mv = Move::from_word(mv);
if let Some(piece) = before.piece_at(mv.from())
&& piece.role != Role::Pawn
{
let occ = before.occupied();
let to = mv.to();
let att = match piece.role {
Role::Knight => KNIGHT_ATT[to.index()],
Role::Bishop => attacks::bishop_attacks(to.0, occ),
Role::Rook => attacks::rook_attacks(to.0, occ),
Role::Queen => attacks::queen_attacks(to.0, occ),
Role::King => KING_ATT[to.index()],
_ => 0,
};
if att & before.piece_bb(piece.color, piece.role) & !(1u64 << mv.from().0) != 0 {
self.stats.prefilter_hits += 1;
}
}
let a = if self.sink_san { move_to_san_body(before, mv) } else { None };
let b = if self.sink_san { san_body_per_candidate(before, mv) } else { None };
if self.sink_san {
match (a, b) {
(Some(x), Some(y)) => {
if x.as_str() != y.as_str() {
self.stats.mismatches += 1;
if self.examples.len() < 5 {
self.examples.push(format!("{} vs {}", x.as_str(), y.as_str()));
}
if self.examples.len() == 1
&& let (Some(piece), Some(want)) =
(before.piece_at(mv.from()), move_to_san_body(before, mv))
{
let to = mv.to();
let occ = before.occupied();
let att = match piece.role {
Role::Knight => KNIGHT_ATT[to.index()],
Role::Bishop => attacks::bishop_attacks(to.0, occ),
Role::Rook => attacks::rook_attacks(to.0, occ),
Role::Queen => attacks::queen_attacks(to.0, occ),
Role::King => KING_ATT[to.index()],
_ => 0,
};
let others = att & before.piece_bb(piece.color, piece.role) & !(1u64 << mv.from().0);
let mut report = format!(
"DIAG move {}{:?} piece {:?} fen {} | gigachess {} | mine {} | candidates:",
mv.from(),
to,
piece,
before.to_fen(),
want.as_str(),
y.as_str()
);
for cand in 0..64u8 {
if others & (1u64 << cand) == 0 {
continue;
}
let cs = Square::new(cand);
let cm = Move::new(cs, to, mv.promotion());
let mut tmp = *before;
let mover = before.turn();
match tmp.play(cm) {
Err(_) => report.push_str(&format!(" {cs}:rejected,")),
Ok(undo) => {
let safe =
tmp.attackers_to(tmp.king_square(mover).0, mover.other(), tmp.occupied())
== 0;
tmp.unmake_move(cm, undo);
let tag = if safe { "legal" } else { "pinned" };
report.push_str(&format!(" {cs}:{tag},"));
}
}
}
self.examples.push(report);
}
} else if x.as_str().len() > 3 {
self.stats.disambiguated += 1;
}
}
_ => self.stats.mismatches += 1,
}
}
if self.gigachess {
std::hint::black_box(move_to_san_body(before, mv));
}
if self.per_candidate {
std::hint::black_box(san_body_per_candidate(before, mv));
}
}
fn played(&mut self, after: &Board) {
if self.suffix {
std::hint::black_box(gigachess::san::check_mate_suffix(after));
return;
}
if self.mate {
self.stats.moves += 1;
if !after.in_check() {
return;
}
self.stats.prefilter_hits += 1; if self.sink_san {
let a = gigachess::san::check_mate_suffix(after);
let mut ml = gigachess::movegen::MoveList::new();
after.generate_moves_into(&mut ml);
let b = if ml.is_empty() { Some('#') } else { Some('+') };
if a != b {
self.stats.mismatches += 1;
}
return;
}
if self.gigachess {
std::hint::black_box(gigachess::san::check_mate_suffix(after));
}
if self.per_candidate {
let mut ml = gigachess::movegen::MoveList::new();
after.generate_moves_into(&mut ml);
std::hint::black_box(!ml.is_empty());
}
}
}
fn branch(&mut self) {}
fn resume(&mut self) {}
fn wants_checkers(&self) -> bool {
true
}
}
fn walk(base: &Path, last: u32, sink: &mut Sink) {
use cbvault_format::cbh::Headers;
use cbvault_format::cbh::moves::GameMoves;
use cbvault_format::file::DbFile;
let headers = Headers::open(base).expect("headers");
let cbg_path = base.with_extension("cbg");
let cbg = DbFile::open(cbg_path.clone()).expect("cbg");
let mut rec = Vec::new();
for id in 1..=last.min(headers.records()) {
let Ok(header) = headers.record(id) else { continue };
let at = u64::from(header.moves_offset());
let mut head = [0u8; 4];
cbg.read_into(at, &mut head).expect("head");
let size = u32::from_be_bytes([0, head[1], head[2], head[3]]) as usize;
rec.clear();
rec.resize(size, 0);
cbg.read_into(at, rec.as_mut_slice()).expect("record");
let game = GameMoves::parse(&cbg_path, &rec).expect("record");
let start = cbvault_chess::decode::start_as_played(GameRef::new(id), &game).expect("start");
if start_board(&start).is_err() {
continue;
}
let _ = walk_from(GameRef::new(id), &game, &start, sink);
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let base = Path::new(&args[1]);
let last: u32 = args[2].parse().expect("last record");
let _ = Color::White;
let mut cmp = Sink { gigachess: false, per_candidate: false, sink_san: true, ..Default::default() };
walk(base, last, &mut cmp);
println!(
"compared {} moves: {} take the ambiguous branch, {} carry a disambiguation, {} MISMATCHES",
cmp.stats.moves, cmp.stats.prefilter_hits, cmp.stats.disambiguated, cmp.stats.mismatches
);
for e in &cmp.examples {
println!(" mismatch: {e}");
}
let mut none = Sink::default();
let t0 = Instant::now();
walk(base, last, &mut none);
let t0s = t0.elapsed();
let mut a = Sink { gigachess: true, ..Default::default() };
let t0 = Instant::now();
walk(base, last, &mut a);
let ta = t0.elapsed();
let mut b = Sink { per_candidate: true, ..Default::default() };
let t0 = Instant::now();
walk(base, last, &mut b);
let tb = t0.elapsed();
let san_a = ta.as_secs_f64() - t0s.as_secs_f64();
let san_b = tb.as_secs_f64() - t0s.as_secs_f64();
println!("walk only: {:.2} s", t0s.as_secs_f64());
println!(
"walk + gigachess SAN: {:.2} s (SAN {:.2} s, {:.1} ns/move)",
ta.as_secs_f64(),
san_a,
san_a * 1e9 / a.stats.moves as f64
);
println!(
"walk + per-candidate SAN: {:.2} s (SAN {:.2} s, {:.1} ns/move)",
tb.as_secs_f64(),
san_b,
san_b * 1e9 / b.stats.moves as f64
);
let mut mc = Sink { sink_san: true, mate: true, ..Default::default() };
walk(base, last, &mut mc);
let mut mg = Sink { gigachess: true, mate: true, ..Default::default() };
let t0 = Instant::now();
walk(base, last, &mut mg);
let tg = t0.elapsed();
let mut me = Sink { per_candidate: true, mate: true, ..Default::default() };
let t0 = Instant::now();
walk(base, last, &mut me);
let te = t0.elapsed();
println!(
"mate test: {} of {} moves leave the side to move in check ({:.1} %), and the two tests disagree on {}",
mc.stats.prefilter_hits,
mc.stats.moves,
100.0 * mc.stats.prefilter_hits as f64 / mc.stats.moves as f64,
mc.stats.mismatches
);
println!(
"walk + gigachess check_mate_suffix: {:.2} s | walk + the ancestor's has-legal-move shape: {:.2} s (delta {:.2} s on this slice)",
tg.as_secs_f64(),
te.as_secs_f64(),
tg.as_secs_f64() - te.as_secs_f64()
);
let mut with = Sink { suffix: true, ..Default::default() };
let t0 = Instant::now();
walk(base, last, &mut with);
let t_suffix = t0.elapsed();
let mut without = Sink::default();
let t0 = Instant::now();
walk(base, last, &mut without);
let t_none = t0.elapsed();
let delta = t_suffix.as_secs_f64() - t_none.as_secs_f64();
println!(
"check/mate suffix: {:.2} s on this slice against {:.2} s without it - {:.1} ns per in-check move, about {:.1} s on a whole-database export",
t_suffix.as_secs_f64(),
t_none.as_secs_f64(),
delta * 1e9 / with.stats.prefilter_hits.max(1) as f64,
delta * (883_141_297.0 / with.stats.moves as f64)
);
println!(
"the ancestor's test costs {:.1} ns/move against gigachess' {:.1} ns: {:.2}x, and would take {:.1} s off a whole-database export",
san_b * 1e9 / b.stats.moves as f64,
san_a * 1e9 / a.stats.moves as f64,
san_a / san_b.max(1e-9),
(san_a - san_b) * (883_141_297.0 / a.stats.moves as f64)
);
}