sekirei 0.3.3

USI shogi engine binary (Sekirei)
//! Statistical opening book, generated by `sekirei-train --build-book` via
//! `lineprior` (<https://github.com/kent-tokyo/lineprior>). `lineprior` owns
//! the JSONL schema, the count/win-rate smoothing, and the confidence
//! scoring/ranking; this module only adds the one thing a domain-agnostic
//! library can't do itself -- mapping its ranked candidate actions back
//! onto real, currently-legal shogi moves.

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()
    }

    /// Walks `sfen`'s candidates in lineprior's own ranked (descending
    /// prior) order, returning the first whose confidence clears
    /// `min_confidence` *and* whose USI string still parses to a legal
    /// move against `board`. Skipping past a low-confidence or now-illegal
    /// entry instead of stopping at the first one is what keeps a stale or
    /// noisy book from ever forcing a bad move -- worst case this returns
    /// `None` and the caller falls back to a normal search, exactly the
    /// designed behavior for an unseen state.
    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();
        // Top-ranked entry (7g7f) has a high prior from one lucky sample but
        // low confidence -- must be skipped in favor of the well-supported one.
        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()
        );
    }
}