rsshogi 1.2.2

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
#![cfg(feature = "book")]

use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use rsshogi::board::hirate_position;
use rsshogi::book::{
    BookDatabase, YaneuraOuAccessMode, YaneuraOuBook, YaneuraOuBookOpenOptions,
    YaneuraOuDb2016WriteOptions, YbbBook,
};
use rsshogi::types::Move;

fn temp_path(suffix: &str) -> PathBuf {
    let nonce = SystemTime::now().duration_since(UNIX_EPOCH).expect("clock after epoch").as_nanos();
    std::env::temp_dir().join(format!("rsshogi-task0026-{}-{nonce}-{suffix}", std::process::id()))
}

#[test]
fn db2016_reader_and_writer_preserve_contract_and_sort_positions() {
    let path = temp_path("book.db");
    let startpos = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1";
    let kings = "4k4/9/9/9/9/9/9/9/4K4 b - 1";
    let input = format!(
        "#YANEURAOU-DB2016 1.00\n\
         sfen {startpos}\n\
         # start entry\n\
         7g7f none 10 3 2 # first move\n\
         sfen {kings}\n\
         # kings entry\n"
    );
    fs::write(&path, input).expect("write DB2016 fixture");

    let options = YaneuraOuBookOpenOptions::with_access_mode(YaneuraOuAccessMode::ScanOnly);
    let book = YaneuraOuBook::open_with_options(&path, options).expect("open DB2016 fixture");
    let mut entries: Vec<_> = book
        .iter_entries()
        .expect("entry iterator")
        .collect::<Result<_, _>>()
        .expect("parse entries");

    assert_eq!(entries[0].comment(), "start entry");
    assert_eq!(entries[0].moves().len(), 1);
    assert_eq!(entries[0].moves()[0].mv(), Move::from_usi("7g7f").unwrap());
    assert_eq!(entries[0].moves()[0].ponder(), Move::MOVE_NONE);
    assert_eq!(entries[0].moves()[0].score(), Some(10));
    assert_eq!(entries[0].moves()[0].depth(), Some(3));
    assert_eq!(entries[0].moves()[0].count(), Some(2));
    assert_eq!(entries[0].moves()[0].comment(), "first move");

    entries.reverse();
    let owned = entries
        .iter()
        .map(BookDatabase::entry_from_yaneuraou)
        .collect::<Result<Vec<_>, _>>()
        .expect("convert entries");
    let database = BookDatabase::try_from_entries(owned).expect("build database");
    let output = database
        .to_yaneuraou_db2016_string(&YaneuraOuDb2016WriteOptions::new())
        .expect("write DB2016");

    assert!(output.starts_with("#YANEURAOU-DB2016 1.00\n"));
    let kings_offset = output.find(&format!("sfen {kings}")).expect("kings row");
    let start_offset = output.find(&format!("sfen {startpos}")).expect("start row");
    assert!(kings_offset < start_offset, "normalized SFEN rows must be sorted");
    assert!(output.contains("# start entry"));
    assert!(output.contains("7g7f none 10 3 2"));
    assert!(output.contains("# first move"));

    let _ = fs::remove_file(path);
}

#[test]
fn db2016_bad_entry_does_not_hide_later_entries() {
    let path = temp_path("recoverable-entry.db");
    let bad = "4k4/9/9/9/9/9/9/9/4K4 b - 1";
    let good = "4k4/9/9/9/9/9/9/4P4/4K4 b - 1";
    let input =
        format!("#YANEURAOU-DB2016 1.00\nsfen {bad}\nK*5e none 0 0\nsfen {good}\n5h5g none 1 2\n");
    fs::write(&path, input).expect("write DB2016 fixture");

    let options = YaneuraOuBookOpenOptions::with_access_mode(YaneuraOuAccessMode::ScanOnly);
    let book = YaneuraOuBook::open_with_options(&path, options).expect("open structural book");
    let mut entries = book.iter_entries().expect("entry iterator");
    assert!(entries.next().expect("bad entry result").is_err());
    assert_eq!(entries.next().expect("good entry result").expect("good entry").sfen(), good);
    assert!(book.lookup_sfen(bad).is_err());
    assert_eq!(book.lookup_sfen(good).expect("lookup later entry").expect("entry").sfen(), good);

    let _ = fs::remove_file(path);
}

#[test]
fn db2016_resign_and_omitted_fields_keep_legacy_meaning() {
    let path = temp_path("omitted-fields.db");
    let startpos = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b -";
    let input =
        format!("#YANEURAOU-DB2016 1.00\nsfen {startpos}\nresign none 0 0\n7g7f none 10 3\n");
    fs::write(&path, input).expect("write DB2016 fixture");

    let options = YaneuraOuBookOpenOptions::with_access_mode(YaneuraOuAccessMode::ScanOnly);
    let book = YaneuraOuBook::open_with_options(&path, options).expect("open DB2016 fixture");
    let entry = book.iter_entries().expect("iterator").next().expect("entry").expect("valid entry");
    assert_eq!(entry.min_ply(), 0);
    assert_eq!(entry.moves()[0].mv(), Move::MOVE_RESIGN);
    assert_eq!(entry.moves()[1].count(), None);

    let database = BookDatabase::try_from_entries(vec![
        BookDatabase::entry_from_yaneuraou(&entry).expect("convert entry"),
    ])
    .expect("database");
    let output = database
        .to_yaneuraou_db2016_string(&YaneuraOuDb2016WriteOptions::new())
        .expect_err("MOVE_NONE is not writable as a candidate");
    assert!(output.to_string().contains("invalid DB2016 move"));

    let path_without_resign = temp_path("omitted-fields-writable.db");
    let input = format!("#YANEURAOU-DB2016 1.00\nsfen {startpos}\n7g7f none 10 3\n");
    fs::write(&path_without_resign, input).expect("write writable fixture");
    let book = YaneuraOuBook::open_with_options(&path_without_resign, options).expect("open book");
    let entry = book.iter_entries().unwrap().next().unwrap().unwrap();
    let database =
        BookDatabase::try_from_entries(vec![BookDatabase::entry_from_yaneuraou(&entry).unwrap()])
            .unwrap();
    let output = database
        .to_yaneuraou_db2016_string(&YaneuraOuDb2016WriteOptions::new())
        .expect("write DB2016");
    assert!(output.contains(&format!("sfen {startpos}\n")));
    assert!(!output.contains(&format!("sfen {startpos} 0\n")));
    assert!(output.contains("7g7f none 10 3\n"));
    assert!(!output.contains("7g7f none 10 3 none"));

    let _ = fs::remove_file(path);
    let _ = fs::remove_file(path_without_resign);
}

#[test]
fn db2016_binary_mode_searches_the_file_without_materializing_it() {
    let path = temp_path("binary-search.db");
    let earlier = "4k4/9/9/9/9/9/9/9/4K4 b - 1";
    let target = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1";
    let padding = "x".repeat(96 * 1024);
    let input = format!(
        "#YANEURAOU-DB2016 1.00\nsfen {earlier}\n# {padding}\nsfen {target}\n7g7f none 1 1\n"
    );
    fs::write(&path, input).expect("write DB2016 fixture");

    let options =
        YaneuraOuBookOpenOptions::with_access_mode(YaneuraOuAccessMode::AssumeSortedByCaller);
    let book = YaneuraOuBook::open_with_options(&path, options).expect("open DB2016 fixture");
    assert_eq!(book.lookup_sfen(earlier).expect("earlier lookup").expect("entry").sfen(), earlier);
    let entry = book.lookup_sfen(target).expect("target lookup").expect("entry");
    assert_eq!(entry.moves()[0].mv(), Move::from_usi("7g7f").unwrap());

    let _ = fs::remove_file(path);
}

#[test]
fn ybb_exact_single_record_layout_is_readable() {
    let path = temp_path("single.ybb");
    let position = hirate_position();
    let packed = position.to_packed_sfen();
    let mv = Move::from_usi("7g7f").expect("move");

    let mut bytes = Vec::new();
    bytes.extend_from_slice(b"YANE-BINBOOK-V1\0");
    bytes.extend_from_slice(&1u64.to_le_bytes());
    bytes.extend_from_slice(&0u64.to_le_bytes());
    bytes.extend_from_slice(packed.as_bytes());
    bytes.extend_from_slice(&0u64.to_le_bytes());
    bytes.extend_from_slice(&position.game_ply().to_le_bytes());
    bytes.extend_from_slice(&1u16.to_le_bytes());
    bytes.extend_from_slice(&mv.raw().to_le_bytes());
    bytes.extend_from_slice(&(-25i16).to_le_bytes());
    fs::write(&path, bytes).expect("write YBB fixture");

    let book = YbbBook::open(&path).expect("open YBB fixture");
    assert_eq!(book.len(), 1);
    assert!(!book.has_depth());
    let entry = book.lookup_position(&position).expect("lookup").expect("entry");
    assert_eq!(entry.packed_sfen(), packed);
    assert_eq!(entry.ply(), position.game_ply());
    assert!(!entry.flipped());
    assert_eq!(entry.moves().len(), 1);
    assert_eq!(entry.moves()[0].mv(), mv);
    assert_eq!(entry.moves()[0].eval(), -25);
    assert_eq!(entry.moves()[0].depth(), None);

    let _ = fs::remove_file(path);
}