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.
///