~alip/jja

10a397998ad0632978c94a51253d55bae03c01c2 — Ali Polatel 11 months ago 6f564fa
restore: lichess eval export -> brainlearn
5 files changed, 170 insertions(+), 4 deletions(-)

M Cargo.lock
M Cargo.toml
M README.md
M src/lib.rs
M src/main.rs
M Cargo.lock => Cargo.lock +11 -1
@@ 872,6 872,15 @@ dependencies = [
]

[[package]]
name = "itertools"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
dependencies = [
 "either",
]

[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 897,6 906,7 @@ dependencies = [
 "i18n-embed",
 "indicatif",
 "is-terminal",
 "itertools 0.12.0",
 "lz4",
 "memmap",
 "nix",


@@ 1682,7 1692,7 @@ dependencies = [
 "arrayvec",
 "bitflags 2.4.1",
 "byteorder",
 "itertools",
 "itertools 0.10.5",
 "once_cell",
 "positioned-io",
 "rustc-hash",

M Cargo.toml => Cargo.toml +1 -0
@@ 42,6 42,7 @@ human-panic = "1.2"
i18n-embed = { version = "0.14", default-features = false, features = ["desktop-requester", "gettext-system", "rust-embed", "tr"], optional = true }
indicatif = "0.17"
is-terminal = "0.4"
itertools = { version = "0.12.0", default-features = false, features = ["use_alloc", "use_std"] }
lz4 = "1.24"
memmap = "0.7"
num_cpus = "1.16"

M README.md => README.md +2 -0
@@ 385,6 385,8 @@ chessboard displaying code in [PolyGlot](http://hgm.nubati.net/book_format.html)

## ?

- Add support for restoring BrainLearn experience files from Lichess
  evaluations export JSON.
- `TotalPlyCount` PGN tag is now supported in make filters.
- fix `--win,draw,loss-factor` calculation for make
- Add 50-moves detection and 3-position repetition detection to the play

M src/lib.rs => src/lib.rs +2 -0
@@ 89,6 89,8 @@ pub mod error;
pub mod file;
/// Utilities for Zobrist Hashing
pub mod hash;
/// Lichess evaluations export file constants and utilities
pub mod lieval;
/// Polyglot Book Merging
pub mod merge;
/// Chessmaster book constants and utilities

M src/main.rs => src/main.rs +154 -3
@@ 70,6 70,8 @@ use anyhow::{anyhow, bail, Context, Result};
use human_panic::setup_panic;
use indicatif::ProgressBar;
use is_terminal::IsTerminal;
use itertools::Itertools;
use jja::lieval::LichessEval;

#[macro_use]
extern crate prettytable;


@@ 442,6 444,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
                        .value_name("output-file")
                        .help(tr!("Path to the chess file")),
                )
                .arg(
                    Arg::new("restore-json")
                        .short('j')
                        .long("json")
                        .action(ArgAction::SetTrue)
                        .help(tr!("Restore from Lichess evaluations export")),
                )
        )
        .subcommand(
            Command::new("edit")


@@ 1797,7 1806,8 @@ Copyright (c) 2016-2020 by Jon Dart
                .get_one::<String>("restore-output")
                .expect("output file");
            let path = Path::new(file);
            command_restore(porcelain, path)
            let json = submatches.get_flag("restore-json");
            command_restore(porcelain, path, json)
                .with_context(|| tr!("Failed to restore file `{}'.", path.display()))?;
        }
        "edit" => {


@@ 3252,7 3262,8 @@ fn polyglot_dump(filename: &str) -> Result<()> {
/// # Arguments
/// * `_porcelain: bool` - Unused parameter.
/// * `path: &Path` - Reference to the path of the file.
fn command_restore(_porcelain: bool, path: &Path) -> Result<()> {
/// * `json: bool` - Restore from Lichess evaluations export
fn command_restore(_porcelain: bool, path: &Path, json: bool) -> Result<()> {
    /* No filetype detection, we rely on file extension,
     * we also do not handle invalid UTF-8 in file extensions.
     */


@@ 3279,7 3290,11 @@ fn command_restore(_porcelain: bool, path: &Path) -> Result<()> {
    if ext == "bin" {
        return polyglot_restore(path);
    } else if ext == "exp" {
        return brainlearn_restore(path);
        return if json {
            brainlearn_restore_json(path)
        } else {
            brainlearn_restore(path)
        };
    } else if ext == "jja-0" {
        /*
         * TODO: This extension is relatively undocumented, and experimental.


@@ 3362,6 3377,142 @@ fn brainlearn_restore(filename: &str) -> Result<()> {
    Ok(())
}

/// `brainlearn_restore_json` function takes a filename and saves all entries read from the
/// standard input into the given Brainlearn experience file.
///
/// # Arguments
/// * `filename: &str` - The filename of the Brainlearn experience file.
fn brainlearn_restore_json(filename: &str) -> Result<()> {
    /* Progress bar */
    let pb = get_progress_bar(0);

    /* Output */
    pb.println(tr!("Creating output BrainLearn experience file..."));
    let mut output_file = BufWriter::new(
        fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(filename)
            .with_context(|| {
                tr!(
                    "Failed to create output BrainLearn experience file `{}'.",
                    filename
                )
            })?,
    );
    pb.println(tr!("Success creating output BrainLearn experience file."));

    /*
     * Read and parse ExperienceEntry instances from standard input, and write them directly to the
     * output book. We apply no processing, such as sorting, here as BrainLearn experience file
     * entries are not sorted.
     */
    pb.println(tr!(
        "Parsing Lichess evaluations export from standard input, and writing them to the output book..."
    ));
    pb.set_message(tr!("Writing:"));
    let mut line_count = 1;
    let mut entry_count = 0;
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let line = line.context(tr!("Failed to read a line from standard input."))?;
        let data: LichessEval = serde_json::from_str(&line)
            .with_context(|| tr!("Failed to parse Lichess evaluation JSON: `{}'.", line))?;
        let epd = match Epd::from_ascii(data.fen.as_bytes()) {
            Ok(epd) => epd,
            Err(error) => {
                pb.println(tr!(
                    "Skipping invalid EPD `{}' on line {}: {}.",
                    data.fen,
                    line_count,
                    error
                ));
                continue;
            }
        };
        let pos: Chess = match epd.into_position(CastlingMode::Standard) {
            Ok(pos) => pos,
            Err(error) => {
                pb.println(tr!(
                    "Skipping illegal EPD `{}' on line {}: {}",
                    data.fen,
                    line_count,
                    error
                ));
                continue;
            }
        };

        let mut moves: HashMap<i32, (i32, i32)> = HashMap::new();
        for entry in data.evals.iter().sorted_by(|a, b| b.depth.cmp(&a.depth)) {
            for pv in &entry.pvs {
                let uci = pv.line.split_whitespace().next().expect("uci move");
                let mov = match Uci::from_ascii(uci.as_bytes()) {
                    Ok(mov) => mov,
                    Err(error) => {
                        pb.println(tr!(
                            "Skipping invalid UCI `{}' on line {}: {}",
                            uci,
                            line_count,
                            error
                        ));
                        continue;
                    }
                };
                let mov = match mov.to_move(&pos) {
                    Ok(mov) => brainlearn::from_move(mov),
                    Err(error) => {
                        pb.println(tr!(
                            "Skipping illegal UCI `{}' on line {}: {}",
                            uci,
                            line_count,
                            error
                        ));
                        continue;
                    }
                };

                if moves.contains_key(&mov) {
                    continue;
                }

                let score = match (pv.cp, pv.mate) {
                    (Some(cp), _) => cp,
                    (None, Some(mate)) => 32000 - mate,
                    (None, None) => panic!("no score or mate on line {line_count}."),
                };
                moves.insert(mov, (entry.depth, score));
            }
        }

        let key = stockfish_hash(&pos);
        for (mov, (depth, score)) in moves.into_iter() {
            let entry = ExperienceEntry {
                key,
                depth,
                score,
                mov,
                perf: 100,
            };
            exp_entry_to_file(&mut output_file, &entry).with_context(|| {
                tr!(
                    "Failed to write to output BrainLearn experience file `{}'.",
                    filename
                )
            })?;
            entry_count += 1;
            pb.inc(1);
        }
        line_count += 1;
    }
    pb.println(tr!(
        "Success restoring {} BrainLearn experience file entries from standard input.",
        entry_count
    ));

    Ok(())
}

/// `polyglot_restore` function takes a filename and saves all entries read from standard input
/// into the given Polyglot opening book file.
///