use std::fs::File;
use std::io::BufReader;
use lineprior::{PriorBook, PriorBookMetadata};
use sekirei_core::board::Board;
use sekirei_core::mv::Move;
use sekirei_core::sfen::move_from_usi;
pub struct Book {
inner: PriorBook,
provenance: BookProvenance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BookProvenance {
Versioned {
schema_version: u32,
producer_version: String,
build_config: String,
build_config_fingerprint: u64,
},
LegacyFingerprint,
Headerless,
}
impl Book {
pub fn load(path: &str) -> Result<Book, String> {
let file = File::open(path).map_err(|e| format!("{path}: {e}"))?;
let loaded = lineprior::load_prior_book_with_metadata(BufReader::new(file))
.map_err(|e| e.to_string())?;
let provenance = match loaded.metadata {
Some(PriorBookMetadata::Versioned(metadata)) => BookProvenance::Versioned {
schema_version: metadata.schema_version,
producer_version: metadata.producer_version,
build_config: metadata.build_config.to_string(),
build_config_fingerprint: metadata.build_config_fingerprint,
},
Some(PriorBookMetadata::LegacyFingerprint { .. }) => BookProvenance::LegacyFingerprint,
Some(_) => return Err("unsupported opening-book metadata variant".to_string()),
None => BookProvenance::Headerless,
};
Ok(Book {
inner: loaded.book,
provenance,
})
}
pub fn len(&self) -> usize {
self.inner.entries.len()
}
pub fn provenance(&self) -> &BookProvenance {
&self.provenance
}
pub fn lookup(&self, sfen: &str, board: &Board, min_confidence: f64) -> Option<Move> {
for action in self.inner.query(sfen, None) {
if action.confidence < min_confidence {
continue;
}
if let Ok(mv) = move_from_usi(&action.action, board) {
return Some(mv);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn load_from(contents: &str) -> Book {
let path = std::env::temp_dir().join(format!(
"sekirei_book_test_{}_{:?}.jsonl",
std::process::id(),
std::thread::current().id()
));
let mut f = File::create(&path).unwrap();
f.write_all(contents.as_bytes()).unwrap();
let book = Book::load(path.to_str().unwrap()).expect("load");
std::fs::remove_file(&path).ok();
book
}
const STARTPOS_SFEN: &str = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1";
#[test]
fn picks_the_highest_ranked_legal_move_above_confidence() {
let book = load_from(&format!(
r#"{{"state":{STARTPOS_SFEN:?},"actions":[{{"action":"7g7f","count":120,"weighted_count":120.0,"success_rate":0.55,"mean_score":0.55,"prior":0.6,"confidence":0.9}},{{"action":"2g2f","count":90,"weighted_count":90.0,"success_rate":0.48,"mean_score":0.48,"prior":0.4,"confidence":0.85}}]}}"#
));
assert_eq!(book.len(), 1);
let board = Board::startpos();
let mv = book
.lookup(STARTPOS_SFEN, &board, 0.2)
.expect("should find a book move");
assert_eq!(sekirei_core::sfen::move_to_usi(mv), "7g7f");
}
#[test]
fn skips_entries_below_min_confidence() {
let book = load_from(&format!(
r#"{{"state":{STARTPOS_SFEN:?},"actions":[{{"action":"7g7f","count":1,"weighted_count":1.0,"success_rate":1.0,"mean_score":1.0,"prior":0.9,"confidence":0.05}},{{"action":"2g2f","count":90,"weighted_count":90.0,"success_rate":0.48,"mean_score":0.48,"prior":0.4,"confidence":0.85}}]}}"#
));
let board = Board::startpos();
let mv = book
.lookup(STARTPOS_SFEN, &board, 0.2)
.expect("should fall through to 2g2f");
assert_eq!(sekirei_core::sfen::move_to_usi(mv), "2g2f");
}
#[test]
fn unseen_state_returns_none() {
let book = load_from(&format!(
r#"{{"state":{STARTPOS_SFEN:?},"actions":[{{"action":"7g7f","count":10,"weighted_count":10.0,"success_rate":0.5,"mean_score":0.5,"prior":0.5,"confidence":0.5}}]}}"#
));
let board = Board::startpos();
assert!(
book.lookup("some-other-sfen-not-in-book", &board, 0.0)
.is_none()
);
}
#[test]
fn loads_lineprior_schema_v1_book() {
let book = load_from(&format!(
concat!(
r#"{{"prior_book_schema_version":1,"producer_version":"0.12.1","build_config":{{}},"build_config_fingerprint":0}}"#,
"\n",
r#"{{"state":{:?},"actions":[{{"action":"7g7f","count":10,"weighted_count":10.0,"success_rate":0.5,"mean_score":0.5,"prior":0.5,"confidence":0.5}}]}}"#
),
STARTPOS_SFEN
));
assert_eq!(book.len(), 1);
assert_eq!(
book.provenance(),
&BookProvenance::Versioned {
schema_version: 1,
producer_version: "0.12.1".to_string(),
build_config: "{}".to_string(),
build_config_fingerprint: 0,
}
);
assert!(
book.lookup(STARTPOS_SFEN, &Board::startpos(), 0.0)
.is_some()
);
}
#[test]
fn distinguishes_headerless_legacy_books() {
let book = load_from(&format!(
r#"{{"state":{STARTPOS_SFEN:?},"actions":[{{"action":"7g7f","count":10,"weighted_count":10.0,"success_rate":0.5,"mean_score":0.5,"prior":0.5,"confidence":0.5}}]}}"#
));
assert_eq!(book.provenance(), &BookProvenance::Headerless);
}
#[test]
fn rejects_unsupported_metadata_schema() {
let path = std::env::temp_dir().join(format!(
"sekirei_book_bad_schema_{}_{:?}.jsonl",
std::process::id(),
std::thread::current().id()
));
std::fs::write(
&path,
r#"{"prior_book_schema_version":99,"producer_version":"future","build_config":{},"build_config_fingerprint":0}"#,
)
.unwrap();
let error = Book::load(path.to_str().unwrap())
.err()
.expect("future schema must be rejected");
std::fs::remove_file(path).ok();
assert!(error.contains("unsupported prior-book schema version"));
}
}