use super::{BookError, BookKey, BookMove, MemoryBook, StaticBook, book_key_from_position};
use crate::board::Position;
use crate::records::formats::traversal;
use crate::records::record::{Record, RecordEntry, RecordNodeId};
use crate::types::Eval;
use crate::types::Move;
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct BookBuilder {
book: MemoryBook,
}
impl BookBuilder {
#[must_use]
pub fn new() -> Self {
Self { book: MemoryBook::new() }
}
pub fn insert(&mut self, key: BookKey, mv: Move, score: i16, depth: u16) {
self.book.insert_move(key, BookMove::new(mv, score, depth));
}
pub fn insert_for_position(&mut self, position: &Position, mv: Move, score: i16, depth: u16) {
let key = book_key_from_position(position);
self.insert(key, mv, score, depth);
}
pub fn extend_from_game_record(
&mut self,
record: &Record,
default_score: i16,
default_depth: u16,
) -> Result<(), BookError> {
let mut root_pos = Position::empty();
root_pos
.set_sfen(record.init_position_sfen())
.map_err(|err| BookError::InvalidData(format!("invalid init sfen: {err:?}")))?;
let root_key = book_key_from_position(&root_pos);
let mut keys: HashMap<RecordNodeId, BookKey> = HashMap::new();
keys.insert(record.root_id(), root_key);
traversal::traverse_with_position(record, |node| match node.entry {
RecordEntry::Move(mv_record) => {
let current_key = book_key_from_position(node.position);
keys.insert(node.node_id, current_key);
let parent_key = keys[&node.parent_id];
let mv = mv_record.mv();
let annotation = record.node(node.node_id).annotation();
let score = annotation.eval().map(Eval::raw).unwrap_or(default_score);
let depth = annotation.depth().unwrap_or(default_depth);
self.insert(parent_key, mv, score, depth);
true
}
RecordEntry::Special(_) => false,
})
.map_err(|err| BookError::InvalidData(format!("traversal failed: {err}")))?;
Ok(())
}
#[must_use]
pub fn into_memory(self) -> MemoryBook {
self.book
}
#[must_use]
pub fn build_static(mut self) -> StaticBook {
for moves in self.book.entries_mut().values_mut() {
sort_moves(moves);
}
StaticBook::from_memory_book(&self.book)
}
}
impl StaticBook {
#[must_use]
pub fn from_memory_book_sorted(book: &MemoryBook) -> Self {
let mut builder = BookBuilder { book: MemoryBook::new() };
for (key, moves) in book.entries() {
builder.book.extend_moves(*key, moves.iter().copied());
}
builder.build_static()
}
}
fn sort_moves(moves: &mut [BookMove]) {
moves.sort_by(|lhs, rhs| {
rhs.score()
.cmp(&lhs.score())
.then_with(|| rhs.depth().cmp(&lhs.depth()))
.then_with(|| lhs.mv().raw().cmp(&rhs.mv().raw()))
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::board::{self, Position};
use crate::records::record::{
EngineInfo, MoveEntry, RecordAnnotation, SpecialMove, SpecialMoveEntry,
};
use crate::types::GameResult;
#[test]
fn test_extend_from_game_record_collects_main_and_variation_moves() {
board::init();
let root = board::hirate_position();
let mv1 = Move::from_usi("7g7f").unwrap();
let mv2 = Move::from_usi("3c3d").unwrap();
let mv_var = Move::from_usi("8c8d").unwrap();
let mut record = Record::new(root.to_sfen(None)).unwrap();
let first = record
.append_move_with_annotation(
record.root_id(),
MoveEntry::new(mv1),
RecordAnnotation::new().with_engine_info(Some(
EngineInfo::new().with_eval(Some(Eval::from_i32(120))).with_depth(Some(18)),
)),
)
.unwrap();
record.append_move(first, MoveEntry::new(mv2)).unwrap();
record
.append_move_with_annotation(
first,
MoveEntry::new(mv_var),
RecordAnnotation::new().with_engine_info(Some(
EngineInfo::new().with_eval(Some(Eval::from_i32(-40))).with_depth(Some(9)),
)),
)
.unwrap();
let mut builder = BookBuilder::default();
builder.extend_from_game_record(&record, 7, 5).unwrap();
let book = builder.into_memory();
let root_key = book_key_from_position(&root);
assert_eq!(book.moves(root_key).unwrap(), &[BookMove::new(mv1, 120, 18)]);
let mut after_mv1 = Position::empty();
after_mv1.set_sfen(record.init_position_sfen()).unwrap();
after_mv1.apply_move(mv1);
let after_mv1_key = book_key_from_position(&after_mv1);
assert_eq!(
book.moves(after_mv1_key).unwrap(),
&[BookMove::new(mv2, 7, 5), BookMove::new(mv_var, -40, 9)]
);
}
#[test]
fn test_extend_from_game_record_skips_special_subtrees() {
board::init();
let root = board::hirate_position();
let mv1 = Move::from_usi("7g7f").unwrap();
let mv2 = Move::from_usi("3c3d").unwrap();
let mut record = Record::new(root.to_sfen(None)).unwrap();
record.extend_main_line(vec![MoveEntry::new(mv1)]).unwrap();
record
.set_main_terminal(SpecialMoveEntry::new(SpecialMove::Interrupt, GameResult::Invalid))
.unwrap();
let result = record.extend_main_line(vec![MoveEntry::new(mv2)]);
assert!(result.is_err());
let mut builder = BookBuilder::default();
builder.extend_from_game_record(&record, 0, 0).unwrap();
let book = builder.into_memory();
let root_key = book_key_from_position(&root);
assert_eq!(book.moves(root_key).unwrap(), &[BookMove::new(mv1, 0, 0)]);
let mut after_mv1 = Position::empty();
after_mv1.set_sfen(record.init_position_sfen()).unwrap();
after_mv1.apply_move(mv1);
let after_mv1_key = book_key_from_position(&after_mv1);
assert!(book.moves(after_mv1_key).is_none());
}
}