use std::fs::File;
use std::io::BufReader;
use lineprior::PriorBook;
use sekirei_core::board::Board;
use sekirei_core::mv::Move;
use sekirei_core::sfen::move_from_usi;
pub struct Book {
inner: PriorBook,
}
impl Book {
pub fn load(path: &str) -> Result<Book, String> {
let file = File::open(path).map_err(|e| format!("{path}: {e}"))?;
let inner = lineprior::load_prior_book(BufReader::new(file)).map_err(|e| e.to_string())?;
Ok(Book { inner })
}
pub fn len(&self) -> usize {
self.inner.entries.len()
}
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()
);
}
}