use chess::{Board, ChessMove, Game, MoveGen};
use indicatif::{ProgressBar, ProgressStyle};
use pgn_reader::{BufferedReader, RawHeader, SanPlus, Skip, Visitor};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use crate::ChessVectorEngine;
#[derive(Debug, Clone)]
pub struct SelfPlayConfig {
pub games_per_iteration: usize,
pub max_moves_per_game: usize,
pub exploration_factor: f32,
pub min_confidence: f32,
pub use_opening_book: bool,
pub temperature: f32,
}
impl Default for SelfPlayConfig {
fn default() -> Self {
Self {
games_per_iteration: 100,
max_moves_per_game: 200,
exploration_factor: 0.3,
min_confidence: 0.1,
use_opening_book: true,
temperature: 0.8,
}
}
}
#[derive(Debug, Clone)]
pub struct TrainingData {
pub board: Board,
pub evaluation: f32,
pub depth: u8,
pub game_id: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TacticalPuzzle {
#[serde(rename = "PuzzleId")]
pub puzzle_id: String,
#[serde(rename = "FEN")]
pub fen: String,
#[serde(rename = "Moves")]
pub moves: String, #[serde(rename = "Rating")]
pub rating: u32,
#[serde(rename = "RatingDeviation")]
pub rating_deviation: u32,
#[serde(rename = "Popularity")]
pub popularity: i32,
#[serde(rename = "NbPlays")]
pub nb_plays: u32,
#[serde(rename = "Themes")]
pub themes: String, #[serde(rename = "GameUrl")]
pub game_url: Option<String>,
#[serde(rename = "OpeningTags")]
pub opening_tags: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TacticalTrainingData {
pub position: Board,
pub solution_move: ChessMove,
pub move_theme: String,
pub difficulty: f32, pub tactical_value: f32, }
impl serde::Serialize for TacticalTrainingData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("TacticalTrainingData", 5)?;
state.serialize_field("fen", &self.position.to_string())?;
state.serialize_field("solution_move", &self.solution_move.to_string())?;
state.serialize_field("move_theme", &self.move_theme)?;
state.serialize_field("difficulty", &self.difficulty)?;
state.serialize_field("tactical_value", &self.tactical_value)?;
state.end()
}
}
impl<'de> serde::Deserialize<'de> for TacticalTrainingData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
struct TacticalTrainingDataVisitor;
impl<'de> Visitor<'de> for TacticalTrainingDataVisitor {
type Value = TacticalTrainingData;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("struct TacticalTrainingData")
}
fn visit_map<V>(self, mut map: V) -> Result<TacticalTrainingData, V::Error>
where
V: MapAccess<'de>,
{
let mut fen = None;
let mut solution_move = None;
let mut move_theme = None;
let mut difficulty = None;
let mut tactical_value = None;
while let Some(key) = map.next_key()? {
match key {
"fen" => {
if fen.is_some() {
return Err(de::Error::duplicate_field("fen"));
}
fen = Some(map.next_value()?);
}
"solution_move" => {
if solution_move.is_some() {
return Err(de::Error::duplicate_field("solution_move"));
}
solution_move = Some(map.next_value()?);
}
"move_theme" => {
if move_theme.is_some() {
return Err(de::Error::duplicate_field("move_theme"));
}
move_theme = Some(map.next_value()?);
}
"difficulty" => {
if difficulty.is_some() {
return Err(de::Error::duplicate_field("difficulty"));
}
difficulty = Some(map.next_value()?);
}
"tactical_value" => {
if tactical_value.is_some() {
return Err(de::Error::duplicate_field("tactical_value"));
}
tactical_value = Some(map.next_value()?);
}
_ => {
let _: serde_json::Value = map.next_value()?;
}
}
}
let fen: String = fen.ok_or_else(|| de::Error::missing_field("fen"))?;
let solution_move_str: String =
solution_move.ok_or_else(|| de::Error::missing_field("solution_move"))?;
let move_theme =
move_theme.ok_or_else(|| de::Error::missing_field("move_theme"))?;
let difficulty =
difficulty.ok_or_else(|| de::Error::missing_field("difficulty"))?;
let tactical_value =
tactical_value.ok_or_else(|| de::Error::missing_field("tactical_value"))?;
let position =
Board::from_str(&fen).map_err(|e| de::Error::custom(format!("Error: {e}")))?;
let solution_move = ChessMove::from_str(&solution_move_str)
.map_err(|e| de::Error::custom(format!("Error: {e}")))?;
Ok(TacticalTrainingData {
position,
solution_move,
move_theme,
difficulty,
tactical_value,
})
}
}
const FIELDS: &[&str] = &[
"fen",
"solution_move",
"move_theme",
"difficulty",
"tactical_value",
];
deserializer.deserialize_struct("TacticalTrainingData", FIELDS, TacticalTrainingDataVisitor)
}
}
pub struct GameExtractor {
pub positions: Vec<TrainingData>,
pub current_game: Game,
pub move_count: usize,
pub max_moves_per_game: usize,
pub game_id: usize,
}
impl GameExtractor {
pub fn new(max_moves_per_game: usize) -> Self {
Self {
positions: Vec::new(),
current_game: Game::new(),
move_count: 0,
max_moves_per_game,
game_id: 0,
}
}
}
impl Visitor for GameExtractor {
type Result = ();
fn begin_game(&mut self) {
self.current_game = Game::new();
self.move_count = 0;
self.game_id += 1;
}
fn header(&mut self, _key: &[u8], _value: RawHeader<'_>) {}
fn san(&mut self, san_plus: SanPlus) {
if self.move_count >= self.max_moves_per_game {
return;
}
let san_str = san_plus.san.to_string();
let current_pos = self.current_game.current_position();
match chess::ChessMove::from_san(¤t_pos, &san_str) {
Ok(chess_move) => {
let legal_moves: Vec<chess::ChessMove> = MoveGen::new_legal(¤t_pos).collect();
if legal_moves.contains(&chess_move) {
if self.current_game.make_move(chess_move) {
self.move_count += 1;
self.positions.push(TrainingData {
board: self.current_game.current_position(),
evaluation: 0.0, depth: 0,
game_id: self.game_id,
});
}
} else {
}
}
Err(_) => {
if !san_str.contains("O-O") && !san_str.contains("=") && san_str.len() > 6 {
}
}
}
}
fn begin_variation(&mut self) -> Skip {
Skip(true) }
fn end_game(&mut self) -> Self::Result {}
}
pub struct ExternalEvaluator {
depth: u8,
}
impl ExternalEvaluator {
pub fn new(depth: u8) -> Self {
Self { depth }
}
pub fn evaluate_position(&self, board: &Board) -> Result<f32, Box<dyn std::error::Error>> {
let mut child = Command::new("stockfish")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdin = child
.stdin
.as_mut()
.ok_or("Failed to get stdin handle for external engine process")?;
let fen = board.to_string();
use std::io::Write;
writeln!(stdin, "uci")?;
writeln!(stdin, "isready")?;
writeln!(stdin, "position fen {fen}")?;
writeln!(stdin, "go depth {}", self.depth)?;
writeln!(stdin, "quit")?;
let output = child.wait_with_output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.starts_with("info") && line.contains("score cp") {
if let Some(cp_pos) = line.find("score cp ") {
let cp_str = &line[cp_pos + 9..];
if let Some(end) = cp_str.find(' ') {
let cp_value = cp_str[..end].parse::<i32>()?;
return Ok(cp_value as f32 / 100.0); }
}
} else if line.starts_with("info") && line.contains("score mate") {
if let Some(mate_pos) = line.find("score mate ") {
let mate_str = &line[mate_pos + 11..];
if let Some(end) = mate_str.find(' ') {
let mate_moves = mate_str[..end].parse::<i32>()?;
return Ok(if mate_moves > 0 { 100.0 } else { -100.0 });
}
}
}
}
Ok(0.0) }
pub fn evaluate_batch(
&self,
positions: &mut [TrainingData],
) -> Result<(), Box<dyn std::error::Error>> {
let pb = ProgressBar::new(positions.len() as u64);
if let Ok(style) = ProgressStyle::default_bar().template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
) {
pb.set_style(style.progress_chars("#>-"));
}
for data in positions.iter_mut() {
match self.evaluate_position(&data.board) {
Ok(eval) => {
data.evaluation = eval;
data.depth = self.depth;
}
Err(e) => {
eprintln!("Evaluation error: {e}");
data.evaluation = 0.0;
}
}
pb.inc(1);
}
pb.finish_with_message("Evaluation complete");
Ok(())
}
pub fn evaluate_batch_parallel(
&self,
positions: &mut [TrainingData],
num_threads: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let pb = ProgressBar::new(positions.len() as u64);
if let Ok(style) = ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Parallel evaluation") {
pb.set_style(style.progress_chars("#>-"));
}
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()?;
pool.install(|| {
positions.par_iter_mut().for_each(|data| {
match self.evaluate_position(&data.board) {
Ok(eval) => {
data.evaluation = eval;
data.depth = self.depth;
}
Err(_) => {
data.evaluation = 0.0;
}
}
pb.inc(1);
});
});
pb.finish_with_message("Parallel evaluation complete");
Ok(())
}
}
struct ExternalEngineProcess {
child: Child,
stdin: BufWriter<std::process::ChildStdin>,
stdout: BufReader<std::process::ChildStdout>,
#[allow(dead_code)]
depth: u8,
}
impl ExternalEngineProcess {
fn new(depth: u8) -> Result<Self, Box<dyn std::error::Error>> {
let mut child = Command::new("stockfish")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdin = BufWriter::new(
child
.stdin
.take()
.ok_or("Failed to get stdin handle for external engine process")?,
);
let stdout = BufReader::new(
child
.stdout
.take()
.ok_or("Failed to get stdout handle for external engine process")?,
);
let mut process = Self {
child,
stdin,
stdout,
depth,
};
process.send_command("uci")?;
process.wait_for_ready()?;
process.send_command("isready")?;
process.wait_for_ready()?;
Ok(process)
}
fn send_command(&mut self, command: &str) -> Result<(), Box<dyn std::error::Error>> {
writeln!(self.stdin, "{command}")?;
self.stdin.flush()?;
Ok(())
}
fn wait_for_ready(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let mut line = String::new();
loop {
line.clear();
self.stdout.read_line(&mut line)?;
if line.trim() == "uciok" || line.trim() == "readyok" {
break;
}
}
Ok(())
}
fn evaluate_position(&mut self, board: &Board) -> Result<f32, Box<dyn std::error::Error>> {
let fen = board.to_string();
self.send_command(&format!("position fen {fen}"))?;
self.send_command(&format!("position fen {fen}"))?;
let mut line = String::new();
let mut last_evaluation = 0.0;
loop {
line.clear();
self.stdout.read_line(&mut line)?;
let line = line.trim();
if line.starts_with("info") && line.contains("score cp") {
if let Some(cp_pos) = line.find("score cp ") {
let cp_str = &line[cp_pos + 9..];
if let Some(end) = cp_str.find(' ') {
if let Ok(cp_value) = cp_str[..end].parse::<i32>() {
last_evaluation = cp_value as f32 / 100.0;
}
}
}
} else if line.starts_with("info") && line.contains("score mate") {
if let Some(mate_pos) = line.find("score mate ") {
let mate_str = &line[mate_pos + 11..];
if let Some(end) = mate_str.find(' ') {
if let Ok(mate_moves) = mate_str[..end].parse::<i32>() {
last_evaluation = if mate_moves > 0 { 100.0 } else { -100.0 };
}
}
}
} else if line.starts_with("bestmove") {
break;
}
}
Ok(last_evaluation)
}
}
impl Drop for ExternalEngineProcess {
fn drop(&mut self) {
let _ = self.send_command("quit");
let _ = self.child.wait();
}
}
pub struct ExternalEnginePool {
pool: Arc<Mutex<Vec<ExternalEngineProcess>>>,
depth: u8,
pool_size: usize,
}
impl ExternalEnginePool {
pub fn new(depth: u8, pool_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
let mut processes = Vec::with_capacity(pool_size);
println!("๐ Initializing external engine pool with {pool_size} processes...");
for i in 0..pool_size {
match ExternalEngineProcess::new(depth) {
Ok(process) => {
processes.push(process);
if i % 2 == 1 {
print!(".");
let _ = std::io::stdout().flush(); }
}
Err(e) => {
eprintln!("Evaluation error: {e}");
return Err(e);
}
}
}
println!(" โ
Pool ready!");
Ok(Self {
pool: Arc::new(Mutex::new(processes)),
depth,
pool_size,
})
}
pub fn evaluate_position(&self, board: &Board) -> Result<f32, Box<dyn std::error::Error>> {
let mut process = {
let mut pool = self.pool.lock().unwrap();
if let Some(process) = pool.pop() {
process
} else {
ExternalEngineProcess::new(self.depth)?
}
};
let result = process.evaluate_position(board);
{
let mut pool = self.pool.lock().unwrap();
if pool.len() < self.pool_size {
pool.push(process);
}
}
result
}
pub fn evaluate_batch_parallel(
&self,
positions: &mut [TrainingData],
) -> Result<(), Box<dyn std::error::Error>> {
let pb = ProgressBar::new(positions.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Pool evaluation")
.unwrap()
.progress_chars("#>-"));
positions.par_iter_mut().for_each(|data| {
match self.evaluate_position(&data.board) {
Ok(eval) => {
data.evaluation = eval;
data.depth = self.depth;
}
Err(_) => {
data.evaluation = 0.0;
}
}
pb.inc(1);
});
pb.finish_with_message("Pool evaluation complete");
Ok(())
}
}
pub struct TrainingDataset {
pub data: Vec<TrainingData>,
}
impl Default for TrainingDataset {
fn default() -> Self {
Self::new()
}
}
impl TrainingDataset {
pub fn new() -> Self {
Self { data: Vec::new() }
}
pub fn load_from_pgn<P: AsRef<Path>>(
&mut self,
path: P,
max_games: Option<usize>,
max_moves_per_game: usize,
) -> Result<(), Box<dyn std::error::Error>> {
use indicatif::{ProgressBar, ProgressStyle};
println!("๐ Reading PGN file...");
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut games = Vec::new();
let mut current_game = String::new();
let mut games_collected = 0;
for line in reader.lines() {
let line = line?;
current_game.push_str(&line);
current_game.push('\n');
if line.trim().ends_with("1-0")
|| line.trim().ends_with("0-1")
|| line.trim().ends_with("1/2-1/2")
|| line.trim().ends_with("*")
{
games.push(current_game.clone());
current_game.clear();
games_collected += 1;
if let Some(max) = max_games {
if games_collected >= max {
break;
}
}
}
}
println!(
"๐ฆ Collected {} games, processing in parallel...",
games.len()
);
let pb = ProgressBar::new(games.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("โก Processing [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
let all_positions: Vec<Vec<TrainingData>> = games
.par_iter()
.map(|game_pgn| {
pb.inc(1);
pb.set_message("Processing game");
let mut local_extractor = GameExtractor::new(max_moves_per_game);
let cursor = std::io::Cursor::new(game_pgn);
let mut reader = BufferedReader::new(cursor);
if let Err(e) = reader.read_all(&mut local_extractor) {
eprintln!("Parse error: {e}");
return Vec::new(); }
local_extractor.positions
})
.collect();
pb.finish_with_message("โ
Parallel processing completed");
for game_positions in all_positions {
self.data.extend(game_positions);
}
println!(
"โ
Loaded {} positions from {} games (parallel processing)",
self.data.len(),
games.len()
);
Ok(())
}
pub fn evaluate_with_external_engine(
&mut self,
depth: u8,
) -> Result<(), Box<dyn std::error::Error>> {
let evaluator = ExternalEvaluator::new(depth);
evaluator.evaluate_batch(&mut self.data)
}
pub fn evaluate_with_external_engine_parallel(
&mut self,
depth: u8,
num_threads: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let evaluator = ExternalEvaluator::new(depth);
evaluator.evaluate_batch_parallel(&mut self.data, num_threads)
}
pub fn train_engine(&self, engine: &mut ChessVectorEngine) {
let pb = ProgressBar::new(self.data.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Training positions")
.unwrap()
.progress_chars("#>-"));
for data in &self.data {
engine.add_position(&data.board, data.evaluation);
pb.inc(1);
}
pb.finish_with_message("Training complete");
println!("Trained engine with {} positions", self.data.len());
}
pub fn split(&self, train_ratio: f32) -> (TrainingDataset, TrainingDataset) {
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::collections::{HashMap, HashSet};
let mut games: HashMap<usize, Vec<&TrainingData>> = HashMap::new();
for data in &self.data {
games.entry(data.game_id).or_default().push(data);
}
let mut game_ids: Vec<usize> = games.keys().cloned().collect();
game_ids.shuffle(&mut thread_rng());
let split_point = (game_ids.len() as f32 * train_ratio) as usize;
let train_game_ids: HashSet<usize> = game_ids[..split_point].iter().cloned().collect();
let mut train_data = Vec::new();
let mut test_data = Vec::new();
for data in &self.data {
if train_game_ids.contains(&data.game_id) {
train_data.push(data.clone());
} else {
test_data.push(data.clone());
}
}
(
TrainingDataset { data: train_data },
TrainingDataset { data: test_data },
)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(&self.data)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let data = serde_json::from_str(&content)?;
Ok(Self { data })
}
pub fn load_and_append<P: AsRef<Path>>(
&mut self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
let existing_len = self.data.len();
let additional_data = Self::load(path)?;
self.data.extend(additional_data.data);
println!(
"Loaded {} additional positions (total: {})",
self.data.len() - existing_len,
self.data.len()
);
Ok(())
}
pub fn merge(&mut self, other: TrainingDataset) {
let existing_len = self.data.len();
self.data.extend(other.data);
println!(
"Merged {} positions (total: {})",
self.data.len() - existing_len,
self.data.len()
);
}
pub fn save_incremental<P: AsRef<Path>>(
&self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
self.save_incremental_with_options(path, true)
}
pub fn save_incremental_with_options<P: AsRef<Path>>(
&self,
path: P,
deduplicate: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let path = path.as_ref();
if path.exists() {
if self.save_append_only(path).is_ok() {
return Ok(());
}
if deduplicate {
self.save_incremental_full_merge(path)
} else {
self.save_incremental_no_dedup(path)
}
} else {
self.save(path)
}
}
fn save_incremental_no_dedup<P: AsRef<Path>>(
&self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
let path = path.as_ref();
println!("๐ Loading existing training data...");
let mut existing = Self::load(path)?;
println!("โก Fast merge without deduplication...");
existing.data.extend(self.data.iter().cloned());
println!(
"๐พ Serializing {} positions to JSON...",
existing.data.len()
);
let json = serde_json::to_string_pretty(&existing.data)?;
println!("โ๏ธ Writing to disk...");
std::fs::write(path, json)?;
println!(
"โ
Fast merge save: total {} positions",
existing.data.len()
);
Ok(())
}
pub fn save_append_only<P: AsRef<Path>>(
&self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
if self.data.is_empty() {
return Ok(());
}
let path = path.as_ref();
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
file.seek(SeekFrom::End(-10))?;
let mut buffer = String::new();
BufReader::new(&file).read_line(&mut buffer)?;
if !buffer.trim().ends_with(']') {
return Err("File doesn't end with JSON array bracket".into());
}
file.seek(SeekFrom::End(-2))?;
write!(file, ",")?;
for (i, data) in self.data.iter().enumerate() {
if i > 0 {
write!(file, ",")?;
}
let json = serde_json::to_string(data)?;
write!(file, "{json}")?;
}
write!(file, "\n]")?;
println!("Fast append: added {} new positions", self.data.len());
Ok(())
}
fn save_incremental_full_merge<P: AsRef<Path>>(
&self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
let path = path.as_ref();
println!("๐ Loading existing training data...");
let mut existing = Self::load(path)?;
let _original_len = existing.data.len();
println!("๐ Streaming merge with deduplication (avoiding O(nยฒ) operation)...");
existing.merge_and_deduplicate(self.data.clone());
println!(
"๐พ Serializing {} positions to JSON...",
existing.data.len()
);
let json = serde_json::to_string_pretty(&existing.data)?;
println!("โ๏ธ Writing to disk...");
std::fs::write(path, json)?;
println!(
"โ
Streaming merge save: total {} positions",
existing.data.len()
);
Ok(())
}
pub fn add_position(&mut self, board: Board, evaluation: f32, depth: u8, game_id: usize) {
self.data.push(TrainingData {
board,
evaluation,
depth,
game_id,
});
}
pub fn next_game_id(&self) -> usize {
self.data.iter().map(|data| data.game_id).max().unwrap_or(0) + 1
}
pub fn deduplicate(&mut self, similarity_threshold: f32) {
if similarity_threshold > 0.999 {
self.deduplicate_fast();
} else {
self.deduplicate_similarity_based(similarity_threshold);
}
}
pub fn deduplicate_fast(&mut self) {
use std::collections::HashSet;
if self.data.is_empty() {
return;
}
let mut seen_positions = HashSet::with_capacity(self.data.len());
let original_len = self.data.len();
self.data.retain(|data| {
let fen = data.board.to_string();
seen_positions.insert(fen)
});
println!(
"Fast deduplicated: {} -> {} positions (removed {} exact duplicates)",
original_len,
self.data.len(),
original_len - self.data.len()
);
}
pub fn merge_and_deduplicate(&mut self, new_data: Vec<TrainingData>) {
use std::collections::HashSet;
if new_data.is_empty() {
return;
}
let _original_len = self.data.len();
let mut existing_positions: HashSet<String> = HashSet::with_capacity(self.data.len());
for data in &self.data {
existing_positions.insert(data.board.to_string());
}
let mut added = 0;
for data in new_data {
let fen = data.board.to_string();
if existing_positions.insert(fen) {
self.data.push(data);
added += 1;
}
}
println!(
"Streaming merge: added {} unique positions (total: {})",
added,
self.data.len()
);
}
fn deduplicate_similarity_based(&mut self, similarity_threshold: f32) {
use crate::PositionEncoder;
use ndarray::Array1;
if self.data.is_empty() {
return;
}
let encoder = PositionEncoder::new(1024);
let mut keep_indices: Vec<bool> = vec![true; self.data.len()];
let vectors: Vec<Array1<f32>> = if self.data.len() > 50 {
self.data
.par_iter()
.map(|data| encoder.encode(&data.board))
.collect()
} else {
self.data
.iter()
.map(|data| encoder.encode(&data.board))
.collect()
};
for i in 1..self.data.len() {
if !keep_indices[i] {
continue;
}
for j in 0..i {
if !keep_indices[j] {
continue;
}
let similarity = Self::cosine_similarity(&vectors[i], &vectors[j]);
if similarity > similarity_threshold {
keep_indices[i] = false;
break;
}
}
}
let original_len = self.data.len();
self.data = self
.data
.iter()
.enumerate()
.filter_map(|(i, data)| {
if keep_indices[i] {
Some(data.clone())
} else {
None
}
})
.collect();
println!(
"Similarity deduplicated: {} -> {} positions (removed {} near-duplicates)",
original_len,
self.data.len(),
original_len - self.data.len()
);
}
pub fn deduplicate_parallel(&mut self, similarity_threshold: f32, chunk_size: usize) {
use crate::PositionEncoder;
use ndarray::Array1;
use std::sync::{Arc, Mutex};
if self.data.is_empty() {
return;
}
let encoder = PositionEncoder::new(1024);
let vectors: Vec<Array1<f32>> = self
.data
.par_iter()
.map(|data| encoder.encode(&data.board))
.collect();
let keep_indices = Arc::new(Mutex::new(vec![true; self.data.len()]));
(1..self.data.len())
.collect::<Vec<_>>()
.par_chunks(chunk_size)
.for_each(|chunk| {
for &i in chunk {
{
let indices = keep_indices.lock().unwrap();
if !indices[i] {
continue;
}
}
for j in 0..i {
{
let indices = keep_indices.lock().unwrap();
if !indices[j] {
continue;
}
}
let similarity = Self::cosine_similarity(&vectors[i], &vectors[j]);
if similarity > similarity_threshold {
let mut indices = keep_indices.lock().unwrap();
indices[i] = false;
break;
}
}
}
});
let keep_indices = keep_indices.lock().unwrap();
let original_len = self.data.len();
self.data = self
.data
.iter()
.enumerate()
.filter_map(|(i, data)| {
if keep_indices[i] {
Some(data.clone())
} else {
None
}
})
.collect();
println!(
"Parallel deduplicated: {} -> {} positions (removed {} duplicates)",
original_len,
self.data.len(),
original_len - self.data.len()
);
}
fn cosine_similarity(a: &ndarray::Array1<f32>, b: &ndarray::Array1<f32>) -> f32 {
let dot_product = a.dot(b);
let norm_a = a.dot(a).sqrt();
let norm_b = b.dot(b).sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
dot_product / (norm_a * norm_b)
}
}
}
pub struct SelfPlayTrainer {
config: SelfPlayConfig,
game_counter: usize,
}
impl SelfPlayTrainer {
pub fn new(config: SelfPlayConfig) -> Self {
Self {
config,
game_counter: 0,
}
}
pub fn generate_training_data(&mut self, engine: &mut ChessVectorEngine) -> TrainingDataset {
let mut dataset = TrainingDataset::new();
println!(
"๐ฎ Starting self-play training with {} games...",
self.config.games_per_iteration
);
let pb = ProgressBar::new(self.config.games_per_iteration as u64);
if let Ok(style) = ProgressStyle::default_bar().template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
) {
pb.set_style(style.progress_chars("#>-"));
}
for _ in 0..self.config.games_per_iteration {
let game_data = self.play_single_game(engine);
dataset.data.extend(game_data);
self.game_counter += 1;
pb.inc(1);
}
pb.finish_with_message("Self-play games completed");
println!(
"โ
Generated {} positions from {} games",
dataset.data.len(),
self.config.games_per_iteration
);
dataset
}
fn play_single_game(&self, engine: &mut ChessVectorEngine) -> Vec<TrainingData> {
let mut game = Game::new();
let mut positions = Vec::new();
let mut move_count = 0;
if self.config.use_opening_book {
if let Some(opening_moves) = self.get_random_opening() {
for mv in opening_moves {
if game.make_move(mv) {
move_count += 1;
} else {
break;
}
}
}
}
while game.result().is_none() && move_count < self.config.max_moves_per_game {
let current_position = game.current_position();
let move_choice = self.select_move_with_exploration(engine, ¤t_position);
if let Some(chess_move) = move_choice {
if let Some(evaluation) = engine.evaluate_position(¤t_position) {
if evaluation.abs() >= self.config.min_confidence || move_count < 10 {
positions.push(TrainingData {
board: current_position,
evaluation,
depth: 1, game_id: self.game_counter,
});
}
}
if !game.make_move(chess_move) {
break; }
move_count += 1;
} else {
break; }
}
if let Some(result) = game.result() {
let final_position = game.current_position();
let final_eval = match result {
chess::GameResult::WhiteCheckmates => {
if final_position.side_to_move() == chess::Color::Black {
10.0
} else {
-10.0
}
}
chess::GameResult::BlackCheckmates => {
if final_position.side_to_move() == chess::Color::White {
10.0
} else {
-10.0
}
}
chess::GameResult::WhiteResigns => -10.0,
chess::GameResult::BlackResigns => 10.0,
chess::GameResult::Stalemate
| chess::GameResult::DrawAccepted
| chess::GameResult::DrawDeclared => 0.0,
};
positions.push(TrainingData {
board: final_position,
evaluation: final_eval,
depth: 1,
game_id: self.game_counter,
});
}
positions
}
fn select_move_with_exploration(
&self,
engine: &mut ChessVectorEngine,
position: &Board,
) -> Option<ChessMove> {
let recommendations = engine.recommend_legal_moves(position, 5);
if recommendations.is_empty() {
return None;
}
if fastrand::f32() < self.config.exploration_factor {
self.select_move_with_temperature(&recommendations)
} else {
Some(recommendations[0].chess_move)
}
}
fn select_move_with_temperature(
&self,
recommendations: &[crate::MoveRecommendation],
) -> Option<ChessMove> {
if recommendations.is_empty() {
return None;
}
let mut probabilities = Vec::new();
let mut sum = 0.0;
for rec in recommendations {
let prob = (rec.average_outcome / self.config.temperature).exp();
probabilities.push(prob);
sum += prob;
}
for prob in &mut probabilities {
*prob /= sum;
}
let rand_val = fastrand::f32();
let mut cumulative = 0.0;
for (i, &prob) in probabilities.iter().enumerate() {
cumulative += prob;
if rand_val <= cumulative {
return Some(recommendations[i].chess_move);
}
}
Some(recommendations[0].chess_move)
}
fn get_random_opening(&self) -> Option<Vec<ChessMove>> {
let openings = [
vec!["e4", "e5", "Nf3", "Nc6", "Bc4"],
vec!["e4", "e5", "Nf3", "Nc6", "Bb5"],
vec!["d4", "d5", "c4"],
vec!["d4", "Nf6", "c4", "g6"],
vec!["e4", "c5"],
vec!["e4", "e6"],
vec!["e4", "c6"],
];
let selected_opening = &openings[fastrand::usize(0..openings.len())];
let mut moves = Vec::new();
let mut game = Game::new();
for move_str in selected_opening {
if let Ok(chess_move) = ChessMove::from_str(move_str) {
if game.make_move(chess_move) {
moves.push(chess_move);
} else {
break;
}
}
}
if moves.is_empty() {
None
} else {
Some(moves)
}
}
}
pub struct EngineEvaluator {
#[allow(dead_code)]
engine_depth: u8,
}
impl EngineEvaluator {
pub fn new(engine_depth: u8) -> Self {
Self { engine_depth }
}
pub fn evaluate_accuracy(
&self,
engine: &mut ChessVectorEngine,
test_data: &TrainingDataset,
) -> Result<f32, Box<dyn std::error::Error>> {
let mut total_error = 0.0;
let mut valid_comparisons = 0;
let pb = ProgressBar::new(test_data.data.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Evaluating accuracy")
.unwrap()
.progress_chars("#>-"));
for data in &test_data.data {
if let Some(engine_eval) = engine.evaluate_position(&data.board) {
let error = (engine_eval - data.evaluation).abs();
total_error += error;
valid_comparisons += 1;
}
pb.inc(1);
}
pb.finish_with_message("Accuracy evaluation complete");
if valid_comparisons > 0 {
let mean_absolute_error = total_error / valid_comparisons as f32;
println!("Mean Absolute Error: {mean_absolute_error:.3} pawns");
println!("Evaluated {valid_comparisons} positions");
Ok(mean_absolute_error)
} else {
Ok(f32::INFINITY)
}
}
}
impl serde::Serialize for TrainingData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("TrainingData", 4)?;
state.serialize_field("fen", &self.board.to_string())?;
state.serialize_field("evaluation", &self.evaluation)?;
state.serialize_field("depth", &self.depth)?;
state.serialize_field("game_id", &self.game_id)?;
state.end()
}
}
impl<'de> serde::Deserialize<'de> for TrainingData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
struct TrainingDataVisitor;
impl<'de> Visitor<'de> for TrainingDataVisitor {
type Value = TrainingData;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("struct TrainingData")
}
fn visit_map<V>(self, mut map: V) -> Result<TrainingData, V::Error>
where
V: MapAccess<'de>,
{
let mut fen = None;
let mut evaluation = None;
let mut depth = None;
let mut game_id = None;
while let Some(key) = map.next_key()? {
match key {
"fen" => {
if fen.is_some() {
return Err(de::Error::duplicate_field("fen"));
}
fen = Some(map.next_value()?);
}
"evaluation" => {
if evaluation.is_some() {
return Err(de::Error::duplicate_field("evaluation"));
}
evaluation = Some(map.next_value()?);
}
"depth" => {
if depth.is_some() {
return Err(de::Error::duplicate_field("depth"));
}
depth = Some(map.next_value()?);
}
"game_id" => {
if game_id.is_some() {
return Err(de::Error::duplicate_field("game_id"));
}
game_id = Some(map.next_value()?);
}
_ => {
let _: serde_json::Value = map.next_value()?;
}
}
}
let fen: String = fen.ok_or_else(|| de::Error::missing_field("fen"))?;
let mut evaluation: f32 =
evaluation.ok_or_else(|| de::Error::missing_field("evaluation"))?;
let depth = depth.ok_or_else(|| de::Error::missing_field("depth"))?;
let game_id = game_id.unwrap_or(0);
if evaluation.abs() > 15.0 {
evaluation /= 100.0;
}
let board =
Board::from_str(&fen).map_err(|e| de::Error::custom(format!("Error: {e}")))?;
Ok(TrainingData {
board,
evaluation,
depth,
game_id,
})
}
}
const FIELDS: &[&str] = &["fen", "evaluation", "depth", "game_id"];
deserializer.deserialize_struct("TrainingData", FIELDS, TrainingDataVisitor)
}
}
pub struct TacticalPuzzleParser;
impl TacticalPuzzleParser {
pub fn parse_csv<P: AsRef<Path>>(
file_path: P,
max_puzzles: Option<usize>,
min_rating: Option<u32>,
max_rating: Option<u32>,
) -> Result<Vec<TacticalTrainingData>, Box<dyn std::error::Error>> {
let file = File::open(&file_path)?;
let file_size = file.metadata()?.len();
if file_size > 100_000_000 {
Self::parse_csv_parallel(file_path, max_puzzles, min_rating, max_rating)
} else {
Self::parse_csv_sequential(file_path, max_puzzles, min_rating, max_rating)
}
}
fn parse_csv_sequential<P: AsRef<Path>>(
file_path: P,
max_puzzles: Option<usize>,
min_rating: Option<u32>,
max_rating: Option<u32>,
) -> Result<Vec<TacticalTrainingData>, Box<dyn std::error::Error>> {
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let mut csv_reader = csv::ReaderBuilder::new()
.has_headers(false)
.flexible(true) .from_reader(reader);
let mut tactical_data = Vec::new();
let mut processed = 0;
let mut skipped = 0;
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} Parsing tactical puzzles: {pos} (skipped: {skipped})")
.unwrap(),
);
for result in csv_reader.records() {
let record = match result {
Ok(r) => r,
Err(e) => {
skipped += 1;
println!("CSV parsing error: {e}");
continue;
}
};
if let Some(puzzle_data) = Self::parse_csv_record(&record, min_rating, max_rating) {
if let Some(tactical_data_item) =
Self::convert_puzzle_to_training_data(&puzzle_data)
{
tactical_data.push(tactical_data_item);
processed += 1;
if let Some(max) = max_puzzles {
if processed >= max {
break;
}
}
} else {
skipped += 1;
}
} else {
skipped += 1;
}
pb.set_message(format!(
"Parsing tactical puzzles: {processed} (skipped: {skipped})"
));
}
pb.finish_with_message(format!("Parsed {processed} puzzles (skipped: {skipped})"));
Ok(tactical_data)
}
fn parse_csv_parallel<P: AsRef<Path>>(
file_path: P,
max_puzzles: Option<usize>,
min_rating: Option<u32>,
max_rating: Option<u32>,
) -> Result<Vec<TacticalTrainingData>, Box<dyn std::error::Error>> {
use std::io::Read;
let mut file = File::open(&file_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let lines: Vec<&str> = contents.lines().collect();
let pb = ProgressBar::new(lines.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Parallel CSV parsing")
.unwrap()
.progress_chars("#>-"));
let tactical_data: Vec<TacticalTrainingData> = lines
.par_iter()
.take(max_puzzles.unwrap_or(usize::MAX))
.filter_map(|line| {
let fields: Vec<&str> = line.split(',').collect();
if fields.len() < 8 {
return None;
}
if let Some(puzzle_data) = Self::parse_csv_fields(&fields, min_rating, max_rating) {
Self::convert_puzzle_to_training_data(&puzzle_data)
} else {
None
}
})
.collect();
pb.finish_with_message(format!(
"Parallel parsing complete: {} puzzles",
tactical_data.len()
));
Ok(tactical_data)
}
fn parse_csv_record(
record: &csv::StringRecord,
min_rating: Option<u32>,
max_rating: Option<u32>,
) -> Option<TacticalPuzzle> {
if record.len() < 8 {
return None;
}
let rating: u32 = record[3].parse().ok()?;
let rating_deviation: u32 = record[4].parse().ok()?;
let popularity: i32 = record[5].parse().ok()?;
let nb_plays: u32 = record[6].parse().ok()?;
if let Some(min) = min_rating {
if rating < min {
return None;
}
}
if let Some(max) = max_rating {
if rating > max {
return None;
}
}
Some(TacticalPuzzle {
puzzle_id: record[0].to_string(),
fen: record[1].to_string(),
moves: record[2].to_string(),
rating,
rating_deviation,
popularity,
nb_plays,
themes: record[7].to_string(),
game_url: if record.len() > 8 {
Some(record[8].to_string())
} else {
None
},
opening_tags: if record.len() > 9 {
Some(record[9].to_string())
} else {
None
},
})
}
fn parse_csv_fields(
fields: &[&str],
min_rating: Option<u32>,
max_rating: Option<u32>,
) -> Option<TacticalPuzzle> {
if fields.len() < 8 {
return None;
}
let rating: u32 = fields[3].parse().ok()?;
let rating_deviation: u32 = fields[4].parse().ok()?;
let popularity: i32 = fields[5].parse().ok()?;
let nb_plays: u32 = fields[6].parse().ok()?;
if let Some(min) = min_rating {
if rating < min {
return None;
}
}
if let Some(max) = max_rating {
if rating > max {
return None;
}
}
Some(TacticalPuzzle {
puzzle_id: fields[0].to_string(),
fen: fields[1].to_string(),
moves: fields[2].to_string(),
rating,
rating_deviation,
popularity,
nb_plays,
themes: fields[7].to_string(),
game_url: if fields.len() > 8 {
Some(fields[8].to_string())
} else {
None
},
opening_tags: if fields.len() > 9 {
Some(fields[9].to_string())
} else {
None
},
})
}
fn convert_puzzle_to_training_data(puzzle: &TacticalPuzzle) -> Option<TacticalTrainingData> {
let position = match Board::from_str(&puzzle.fen) {
Ok(board) => board,
Err(_) => return None,
};
let moves: Vec<&str> = puzzle.moves.split_whitespace().collect();
if moves.is_empty() {
return None;
}
let solution_move = match ChessMove::from_str(moves[0]) {
Ok(mv) => mv,
Err(_) => {
match ChessMove::from_san(&position, moves[0]) {
Ok(mv) => mv,
Err(_) => return None,
}
}
};
let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&position).collect();
if !legal_moves.contains(&solution_move) {
return None;
}
let themes: Vec<&str> = puzzle.themes.split_whitespace().collect();
let primary_theme = themes.first().unwrap_or(&"tactical").to_string();
let difficulty = puzzle.rating as f32 / 1000.0; let popularity_bonus = (puzzle.popularity as f32 / 100.0).min(2.0);
let tactical_value = difficulty + popularity_bonus;
Some(TacticalTrainingData {
position,
solution_move,
move_theme: primary_theme,
difficulty,
tactical_value,
})
}
pub fn load_into_engine(
tactical_data: &[TacticalTrainingData],
engine: &mut ChessVectorEngine,
) {
let pb = ProgressBar::new(tactical_data.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Loading tactical patterns")
.unwrap()
.progress_chars("#>-"));
for data in tactical_data {
engine.add_position_with_move(
&data.position,
0.0, Some(data.solution_move),
Some(data.tactical_value), );
pb.inc(1);
}
pb.finish_with_message(format!("Loaded {} tactical patterns", tactical_data.len()));
}
pub fn load_into_engine_incremental(
tactical_data: &[TacticalTrainingData],
engine: &mut ChessVectorEngine,
) {
let initial_size = engine.knowledge_base_size();
let initial_moves = engine.position_moves.len();
if tactical_data.len() > 1000 {
Self::load_into_engine_incremental_parallel(
tactical_data,
engine,
initial_size,
initial_moves,
);
} else {
Self::load_into_engine_incremental_sequential(
tactical_data,
engine,
initial_size,
initial_moves,
);
}
}
fn load_into_engine_incremental_sequential(
tactical_data: &[TacticalTrainingData],
engine: &mut ChessVectorEngine,
initial_size: usize,
initial_moves: usize,
) {
let pb = ProgressBar::new(tactical_data.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Loading tactical patterns (incremental)")
.unwrap()
.progress_chars("#>-"));
let mut added = 0;
let mut skipped = 0;
for data in tactical_data {
if !engine.position_boards.contains(&data.position) {
engine.add_position_with_move(
&data.position,
0.0, Some(data.solution_move),
Some(data.tactical_value), );
added += 1;
} else {
skipped += 1;
}
pb.inc(1);
}
pb.finish_with_message(format!(
"Loaded {} new tactical patterns (skipped {} duplicates, total: {})",
added,
skipped,
engine.knowledge_base_size()
));
println!("Incremental tactical training:");
println!(
" - Positions: {} โ {} (+{})",
initial_size,
engine.knowledge_base_size(),
engine.knowledge_base_size() - initial_size
);
println!(
" - Move entries: {} โ {} (+{})",
initial_moves,
engine.position_moves.len(),
engine.position_moves.len() - initial_moves
);
}
fn load_into_engine_incremental_parallel(
tactical_data: &[TacticalTrainingData],
engine: &mut ChessVectorEngine,
initial_size: usize,
initial_moves: usize,
) {
let pb = ProgressBar::new(tactical_data.len() as u64);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} Optimized batch loading tactical patterns")
.unwrap()
.progress_chars("#>-"));
let filtered_data: Vec<&TacticalTrainingData> = tactical_data
.par_iter()
.filter(|data| !engine.position_boards.contains(&data.position))
.collect();
let batch_size = 1000; let mut added = 0;
println!(
"Pre-filtered: {} โ {} positions (removed {} duplicates)",
tactical_data.len(),
filtered_data.len(),
tactical_data.len() - filtered_data.len()
);
for batch in filtered_data.chunks(batch_size) {
let batch_start = added;
for data in batch {
if !engine.position_boards.contains(&data.position) {
engine.add_position_with_move(
&data.position,
0.0, Some(data.solution_move),
Some(data.tactical_value), );
added += 1;
}
pb.inc(1);
}
pb.set_message(format!("Loaded batch: {} positions", added - batch_start));
}
let skipped = tactical_data.len() - added;
pb.finish_with_message(format!(
"Optimized loaded {} new tactical patterns (skipped {} duplicates, total: {})",
added,
skipped,
engine.knowledge_base_size()
));
println!("Incremental tactical training (optimized):");
println!(
" - Positions: {} โ {} (+{})",
initial_size,
engine.knowledge_base_size(),
engine.knowledge_base_size() - initial_size
);
println!(
" - Move entries: {} โ {} (+{})",
initial_moves,
engine.position_moves.len(),
engine.position_moves.len() - initial_moves
);
println!(
" - Batch size: {}, Pre-filtered efficiency: {:.1}%",
batch_size,
(filtered_data.len() as f32 / tactical_data.len() as f32) * 100.0
);
}
pub fn save_tactical_puzzles<P: AsRef<std::path::Path>>(
tactical_data: &[TacticalTrainingData],
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(tactical_data)?;
std::fs::write(path, json)?;
println!("Saved {} tactical puzzles", tactical_data.len());
Ok(())
}
pub fn load_tactical_puzzles<P: AsRef<std::path::Path>>(
path: P,
) -> Result<Vec<TacticalTrainingData>, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let tactical_data: Vec<TacticalTrainingData> = serde_json::from_str(&content)?;
println!("Loaded {} tactical puzzles from file", tactical_data.len());
Ok(tactical_data)
}
pub fn save_tactical_puzzles_incremental<P: AsRef<std::path::Path>>(
tactical_data: &[TacticalTrainingData],
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
let path = path.as_ref();
if path.exists() {
let mut existing = Self::load_tactical_puzzles(path)?;
let original_len = existing.len();
for new_puzzle in tactical_data {
let exists = existing.iter().any(|existing_puzzle| {
existing_puzzle.position == new_puzzle.position
&& existing_puzzle.solution_move == new_puzzle.solution_move
});
if !exists {
existing.push(new_puzzle.clone());
}
}
let json = serde_json::to_string_pretty(&existing)?;
std::fs::write(path, json)?;
println!(
"Incremental save: added {} new puzzles (total: {})",
existing.len() - original_len,
existing.len()
);
} else {
Self::save_tactical_puzzles(tactical_data, path)?;
}
Ok(())
}
pub fn parse_and_load_incremental<P: AsRef<std::path::Path>>(
file_path: P,
engine: &mut ChessVectorEngine,
max_puzzles: Option<usize>,
min_rating: Option<u32>,
max_rating: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Parsing Lichess puzzles incrementally...");
let tactical_data = Self::parse_csv(file_path, max_puzzles, min_rating, max_rating)?;
Self::load_into_engine_incremental(&tactical_data, engine);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chess::Board;
use std::str::FromStr;
#[test]
fn test_training_dataset_creation() {
let dataset = TrainingDataset::new();
assert_eq!(dataset.data.len(), 0);
}
#[test]
fn test_add_training_data() {
let mut dataset = TrainingDataset::new();
let board = Board::default();
let training_data = TrainingData {
board,
evaluation: 0.5,
depth: 15,
game_id: 1,
};
dataset.data.push(training_data);
assert_eq!(dataset.data.len(), 1);
assert_eq!(dataset.data[0].evaluation, 0.5);
}
#[test]
fn test_chess_engine_integration() {
let mut dataset = TrainingDataset::new();
let board = Board::default();
let training_data = TrainingData {
board,
evaluation: 0.3,
depth: 15,
game_id: 1,
};
dataset.data.push(training_data);
let mut engine = ChessVectorEngine::new(1024);
dataset.train_engine(&mut engine);
assert_eq!(engine.knowledge_base_size(), 1);
let eval = engine.evaluate_position(&board);
assert!(eval.is_some());
let eval_value = eval.unwrap();
assert!(
eval_value > -1000.0 && eval_value < 1000.0,
"Evaluation should be reasonable: {}",
eval_value
);
}
#[test]
fn test_deduplication() {
let mut dataset = TrainingDataset::new();
let board = Board::default();
for i in 0..5 {
let training_data = TrainingData {
board,
evaluation: i as f32 * 0.1,
depth: 15,
game_id: i,
};
dataset.data.push(training_data);
}
assert_eq!(dataset.data.len(), 5);
dataset.deduplicate(0.999);
assert_eq!(dataset.data.len(), 1);
}
#[test]
fn test_dataset_serialization() {
let mut dataset = TrainingDataset::new();
let board =
Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap();
let training_data = TrainingData {
board,
evaluation: 0.2,
depth: 10,
game_id: 42,
};
dataset.data.push(training_data);
let json = serde_json::to_string(&dataset.data).unwrap();
let loaded_data: Vec<TrainingData> = serde_json::from_str(&json).unwrap();
let loaded_dataset = TrainingDataset { data: loaded_data };
assert_eq!(loaded_dataset.data.len(), 1);
assert_eq!(loaded_dataset.data[0].evaluation, 0.2);
assert_eq!(loaded_dataset.data[0].depth, 10);
assert_eq!(loaded_dataset.data[0].game_id, 42);
}
#[test]
fn test_tactical_puzzle_processing() {
let puzzle = TacticalPuzzle {
puzzle_id: "test123".to_string(),
fen: "r1bqkbnr/pppp1ppp/2n5/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4".to_string(),
moves: "Bxf7+ Ke7".to_string(),
rating: 1500,
rating_deviation: 100,
popularity: 150,
nb_plays: 1000,
themes: "fork pin".to_string(),
game_url: None,
opening_tags: None,
};
let tactical_data = TacticalPuzzleParser::convert_puzzle_to_training_data(&puzzle);
assert!(tactical_data.is_some());
let data = tactical_data.unwrap();
assert_eq!(data.move_theme, "fork");
assert!(data.tactical_value > 1.0); assert!(data.difficulty > 0.0);
}
#[test]
fn test_tactical_puzzle_invalid_fen() {
let puzzle = TacticalPuzzle {
puzzle_id: "test123".to_string(),
fen: "invalid_fen".to_string(),
moves: "e2e4".to_string(),
rating: 1500,
rating_deviation: 100,
popularity: 150,
nb_plays: 1000,
themes: "tactics".to_string(),
game_url: None,
opening_tags: None,
};
let tactical_data = TacticalPuzzleParser::convert_puzzle_to_training_data(&puzzle);
assert!(tactical_data.is_none());
}
#[test]
fn test_engine_evaluator() {
let evaluator = EngineEvaluator::new(15);
let mut dataset = TrainingDataset::new();
let board = Board::default();
let training_data = TrainingData {
board,
evaluation: 0.0,
depth: 15,
game_id: 1,
};
dataset.data.push(training_data);
let mut engine = ChessVectorEngine::new(1024);
engine.add_position(&board, 0.1);
let accuracy = evaluator.evaluate_accuracy(&mut engine, &dataset);
assert!(accuracy.is_ok());
let accuracy_value = accuracy.unwrap();
assert!(
accuracy_value >= 0.0,
"Accuracy should be non-negative: {}",
accuracy_value
);
}
#[test]
fn test_tactical_training_integration() {
let tactical_data = vec![TacticalTrainingData {
position: Board::default(),
solution_move: ChessMove::from_str("e2e4").unwrap(),
move_theme: "opening".to_string(),
difficulty: 1.2,
tactical_value: 2.5,
}];
let mut engine = ChessVectorEngine::new(1024);
TacticalPuzzleParser::load_into_engine(&tactical_data, &mut engine);
assert_eq!(engine.knowledge_base_size(), 1);
assert_eq!(engine.position_moves.len(), 1);
let recommendations = engine.recommend_moves(&Board::default(), 5);
assert!(!recommendations.is_empty());
}
#[test]
fn test_multithreading_operations() {
let mut dataset = TrainingDataset::new();
let board = Board::default();
for i in 0..10 {
let training_data = TrainingData {
board,
evaluation: i as f32 * 0.1,
depth: 15,
game_id: i,
};
dataset.data.push(training_data);
}
dataset.deduplicate_parallel(0.95, 5);
assert!(dataset.data.len() <= 10);
}
#[test]
fn test_incremental_dataset_operations() {
let mut dataset1 = TrainingDataset::new();
let board1 = Board::default();
let board2 =
Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap();
dataset1.add_position(board1, 0.0, 15, 1);
dataset1.add_position(board2, 0.2, 15, 2);
assert_eq!(dataset1.data.len(), 2);
let mut dataset2 = TrainingDataset::new();
dataset2.add_position(
Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
.unwrap(),
0.3,
15,
3,
);
dataset1.merge(dataset2);
assert_eq!(dataset1.data.len(), 3);
let next_id = dataset1.next_game_id();
assert_eq!(next_id, 4); }
#[test]
fn test_save_load_incremental() {
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("incremental_test.json");
let mut dataset1 = TrainingDataset::new();
dataset1.add_position(Board::default(), 0.0, 15, 1);
dataset1.save(&file_path).unwrap();
let mut dataset2 = TrainingDataset::new();
dataset2.add_position(
Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap(),
0.2,
15,
2,
);
dataset2.save_incremental(&file_path).unwrap();
let loaded = TrainingDataset::load(&file_path).unwrap();
assert_eq!(loaded.data.len(), 2);
let mut dataset3 = TrainingDataset::new();
dataset3.add_position(
Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
.unwrap(),
0.3,
15,
3,
);
dataset3.load_and_append(&file_path).unwrap();
assert_eq!(dataset3.data.len(), 3); }
#[test]
fn test_add_position_method() {
let mut dataset = TrainingDataset::new();
let board = Board::default();
dataset.add_position(board, 0.5, 20, 42);
assert_eq!(dataset.data.len(), 1);
assert_eq!(dataset.data[0].evaluation, 0.5);
assert_eq!(dataset.data[0].depth, 20);
assert_eq!(dataset.data[0].game_id, 42);
}
#[test]
fn test_incremental_save_deduplication() {
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("dedup_test.json");
let mut dataset1 = TrainingDataset::new();
dataset1.add_position(Board::default(), 0.0, 15, 1);
dataset1.save(&file_path).unwrap();
let mut dataset2 = TrainingDataset::new();
dataset2.add_position(Board::default(), 0.1, 15, 2); dataset2.save_incremental(&file_path).unwrap();
let loaded = TrainingDataset::load(&file_path).unwrap();
assert_eq!(loaded.data.len(), 1);
}
#[test]
fn test_tactical_puzzle_incremental_loading() {
let tactical_data = vec![
TacticalTrainingData {
position: Board::default(),
solution_move: ChessMove::from_str("e2e4").unwrap(),
move_theme: "opening".to_string(),
difficulty: 1.2,
tactical_value: 2.5,
},
TacticalTrainingData {
position: Board::from_str(
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
)
.unwrap(),
solution_move: ChessMove::from_str("e7e5").unwrap(),
move_theme: "opening".to_string(),
difficulty: 1.0,
tactical_value: 2.0,
},
];
let mut engine = ChessVectorEngine::new(1024);
engine.add_position(&Board::default(), 0.1);
assert_eq!(engine.knowledge_base_size(), 1);
TacticalPuzzleParser::load_into_engine_incremental(&tactical_data, &mut engine);
assert_eq!(engine.knowledge_base_size(), 2);
assert!(engine.training_stats().has_move_data);
assert!(engine.training_stats().move_data_entries > 0);
}
#[test]
fn test_tactical_puzzle_serialization() {
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("tactical_test.json");
let tactical_data = vec![TacticalTrainingData {
position: Board::default(),
solution_move: ChessMove::from_str("e2e4").unwrap(),
move_theme: "fork".to_string(),
difficulty: 1.5,
tactical_value: 3.0,
}];
TacticalPuzzleParser::save_tactical_puzzles(&tactical_data, &file_path).unwrap();
let loaded = TacticalPuzzleParser::load_tactical_puzzles(&file_path).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].move_theme, "fork");
assert_eq!(loaded[0].difficulty, 1.5);
assert_eq!(loaded[0].tactical_value, 3.0);
}
#[test]
fn test_tactical_puzzle_incremental_save() {
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("incremental_tactical.json");
let batch1 = vec![TacticalTrainingData {
position: Board::default(),
solution_move: ChessMove::from_str("e2e4").unwrap(),
move_theme: "opening".to_string(),
difficulty: 1.0,
tactical_value: 2.0,
}];
TacticalPuzzleParser::save_tactical_puzzles(&batch1, &file_path).unwrap();
let batch2 = vec![TacticalTrainingData {
position: Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1")
.unwrap(),
solution_move: ChessMove::from_str("e7e5").unwrap(),
move_theme: "counter".to_string(),
difficulty: 1.2,
tactical_value: 2.2,
}];
TacticalPuzzleParser::save_tactical_puzzles_incremental(&batch2, &file_path).unwrap();
let loaded = TacticalPuzzleParser::load_tactical_puzzles(&file_path).unwrap();
assert_eq!(loaded.len(), 2);
}
#[test]
fn test_tactical_puzzle_incremental_deduplication() {
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("dedup_tactical.json");
let tactical_data = TacticalTrainingData {
position: Board::default(),
solution_move: ChessMove::from_str("e2e4").unwrap(),
move_theme: "opening".to_string(),
difficulty: 1.0,
tactical_value: 2.0,
};
TacticalPuzzleParser::save_tactical_puzzles(&[tactical_data.clone()], &file_path).unwrap();
TacticalPuzzleParser::save_tactical_puzzles_incremental(&[tactical_data], &file_path)
.unwrap();
let loaded = TacticalPuzzleParser::load_tactical_puzzles(&file_path).unwrap();
assert_eq!(loaded.len(), 1);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningProgress {
pub iterations_completed: usize,
pub total_games_played: usize,
pub positions_generated: usize,
pub positions_kept: usize,
pub average_position_quality: f32,
pub best_positions_found: usize,
pub training_start_time: Option<std::time::SystemTime>,
pub last_update_time: Option<std::time::SystemTime>,
pub elo_progression: Vec<(usize, f32)>, }
impl Default for LearningProgress {
fn default() -> Self {
Self {
iterations_completed: 0,
total_games_played: 0,
positions_generated: 0,
positions_kept: 0,
average_position_quality: 0.0,
best_positions_found: 0,
training_start_time: Some(std::time::SystemTime::now()),
last_update_time: Some(std::time::SystemTime::now()),
elo_progression: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedSelfLearningSystem {
pub quality_threshold: f32,
pub max_positions: usize,
pub pattern_confidence_threshold: f32,
pub games_per_iteration: usize,
pub improvement_threshold: f32,
pub learning_stats: LearningProgress,
}
impl Default for AdvancedSelfLearningSystem {
fn default() -> Self {
Self {
quality_threshold: 0.6, max_positions: 500_000, pattern_confidence_threshold: 0.75, games_per_iteration: 20, improvement_threshold: 0.1, learning_stats: LearningProgress::default(),
}
}
}
impl AdvancedSelfLearningSystem {
pub fn new(quality_threshold: f32, max_positions: usize) -> Self {
Self {
quality_threshold,
max_positions,
..Default::default()
}
}
pub fn new_with_config(
quality_threshold: f32,
max_positions: usize,
games_per_iteration: usize,
) -> Self {
Self {
quality_threshold,
max_positions,
games_per_iteration,
..Default::default()
}
}
pub fn save_progress<P: AsRef<std::path::Path>>(
&self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
println!("๐พ Saved learning progress");
Ok(())
}
pub fn load_progress<P: AsRef<std::path::Path>>(
path: P,
) -> Result<Self, Box<dyn std::error::Error>> {
if path.as_ref().exists() {
let json = std::fs::read_to_string(path)?;
let mut system: Self = serde_json::from_str(&json)?;
system.learning_stats.last_update_time = Some(std::time::SystemTime::now());
println!(
"๐ Loaded learning progress: {} iterations, {} games played",
system.learning_stats.iterations_completed,
system.learning_stats.total_games_played
);
Ok(system)
} else {
println!("๐ No progress file found, starting fresh");
Ok(Self::default())
}
}
pub fn get_progress_report(&self) -> String {
let total_time = if let Some(start) = self.learning_stats.training_start_time {
match std::time::SystemTime::now().duration_since(start) {
Ok(duration) => format!("{:.1} hours", duration.as_secs_f64() / 3600.0),
Err(_) => "Unknown".to_string(),
}
} else {
"Unknown".to_string()
};
let latest_elo = self
.learning_stats
.elo_progression
.last()
.map(|(_, elo)| format!("{:.0}", elo))
.unwrap_or_else(|| "Unknown".to_string());
format!(
"๐ง Advanced Self-Learning Progress Report\n\
==========================================\n\
Training Duration: {}\n\
Iterations Completed: {}\n\
Total Games Played: {}\n\
Positions Generated: {}\n\
Positions Kept: {} ({:.1}% quality)\n\
Best Positions Found: {}\n\
Average Position Quality: {:.3}\n\
Latest Estimated ELO: {}\n\
ELO Progression: {} data points\n\
\n\
๐ก Ready for external engine testing!",
total_time,
self.learning_stats.iterations_completed,
self.learning_stats.total_games_played,
self.learning_stats.positions_generated,
self.learning_stats.positions_kept,
if self.learning_stats.positions_generated > 0 {
self.learning_stats.positions_kept as f32
/ self.learning_stats.positions_generated as f32
* 100.0
} else {
0.0
},
self.learning_stats.best_positions_found,
self.learning_stats.average_position_quality,
latest_elo,
self.learning_stats.elo_progression.len()
)
}
pub fn continuous_learning_iteration(
&mut self,
engine: &mut ChessVectorEngine,
) -> Result<LearningStats, Box<dyn std::error::Error>> {
println!("๐ง Starting continuous learning iteration...");
let mut stats = LearningStats::new();
let new_positions = self.generate_intelligent_positions(engine)?;
stats.positions_generated = new_positions.len();
let original_count = new_positions.len();
let filtered_positions = if self.games_per_iteration <= 10 {
println!("โก Fast mode: Skipping expensive position filtering...");
new_positions
} else {
self.filter_bad_positions(&new_positions, engine)?
};
if self.games_per_iteration > 10 {
println!(
"๐ Filtered: {} โ {} positions (removed {} bad positions)",
original_count,
filtered_positions.len(),
original_count - filtered_positions.len()
);
}
let quality_positions = if self.games_per_iteration <= 10 {
self.evaluate_position_quality_fast(&filtered_positions)?
} else {
self.evaluate_position_quality(&filtered_positions, engine)?
};
stats.positions_kept = quality_positions.len();
let pruned_count = self.prune_low_quality_positions_with_progress(engine)?;
stats.positions_pruned = pruned_count;
self.add_positions_with_progress(&quality_positions, engine, &mut stats)?;
self.optimize_vector_database_with_progress(engine)?;
self.learning_stats.iterations_completed += 1;
self.learning_stats.total_games_played += self.games_per_iteration;
self.learning_stats.positions_generated += stats.positions_generated;
self.learning_stats.positions_kept += stats.positions_kept;
self.learning_stats.best_positions_found += stats.high_quality_positions;
self.learning_stats.last_update_time = Some(std::time::SystemTime::now());
if stats.positions_kept > 0 {
self.learning_stats.average_position_quality =
(self.learning_stats.average_position_quality
* (self.learning_stats.iterations_completed - 1) as f32
+ stats.high_quality_positions as f32 / stats.positions_kept as f32)
/ self.learning_stats.iterations_completed as f32;
}
let estimated_elo = 1000.0
+ (self.learning_stats.positions_kept as f32 * 0.1)
+ (self.learning_stats.average_position_quality * 500.0)
+ (self.learning_stats.iterations_completed as f32 * 10.0);
self.learning_stats
.elo_progression
.push((self.learning_stats.iterations_completed, estimated_elo));
println!(
"โ
Learning iteration complete: {} positions generated, {} kept, {} pruned",
stats.positions_generated, stats.positions_kept, stats.positions_pruned
);
println!(
"๐ Estimated ELO: {:.0} (+{:.0})",
estimated_elo,
if self.learning_stats.elo_progression.len() > 1 {
estimated_elo
- self.learning_stats.elo_progression
[self.learning_stats.elo_progression.len() - 2]
.1
} else {
0.0
}
);
Ok(stats)
}
fn generate_intelligent_positions(
&self,
_engine: &mut ChessVectorEngine,
) -> Result<Vec<(Board, f32)>, Box<dyn std::error::Error>> {
use indicatif::{ProgressBar, ProgressStyle};
use std::time::{Duration, Instant};
let mut positions = Vec::new();
let start_time = Instant::now();
let timeout_duration = Duration::from_secs(300);
let adaptive_games = if self.games_per_iteration > 10 {
println!(
"โก Using fast parallel mode for {} games...",
self.games_per_iteration
);
self.games_per_iteration
} else {
self.games_per_iteration
};
println!(
"๐ฎ Generating {} intelligent self-play games (5min timeout)...",
adaptive_games
);
let pb = ProgressBar::new(adaptive_games as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("โก Self-Play [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
let game_numbers: Vec<usize> = (0..adaptive_games).collect();
println!(
"๐ฅ Starting parallel self-play with {} CPU cores...",
num_cpus::get()
);
let all_positions: Vec<Vec<(Board, f32)>> = game_numbers
.par_iter()
.map(|&game_num| {
if start_time.elapsed() > timeout_duration {
pb.set_message("โฐ Timeout reached");
return Vec::new();
}
pb.set_message(format!("Game {}", game_num + 1));
let result = self
.play_quick_focused_game(game_num)
.unwrap_or_else(|_| Vec::new());
pb.inc(1);
result
})
.collect();
for game_positions in all_positions {
positions.extend(game_positions);
}
let elapsed = start_time.elapsed();
if elapsed > timeout_duration {
println!("โฐ Self-play timed out after {} seconds", elapsed.as_secs());
}
pb.finish_with_message("โ
Self-play games completed");
println!(
"๐ฏ Generated {} candidate positions from self-play",
positions.len()
);
Ok(positions)
}
#[allow(dead_code)]
fn play_focused_game(
&self,
engine: &mut ChessVectorEngine,
game_id: usize,
) -> Result<Vec<(Board, f32)>, Box<dyn std::error::Error>> {
let mut game = Game::new();
let mut positions = Vec::new();
let mut move_count = 0;
let opening_strategy = game_id % 4;
self.apply_opening_strategy(&mut game, opening_strategy)?;
while game.result().is_none() && move_count < 150 {
let current_position = game.current_position();
if let Some(evaluation) = engine.evaluate_position(¤t_position) {
if self.is_strategic_position(¤t_position)
&& evaluation.abs() < 3.0 && self.is_novel_position(¤t_position, engine)
{
positions.push((current_position, evaluation));
}
}
if let Some(chess_move) = self.select_strategic_move(engine, ¤t_position) {
if !game.make_move(chess_move) {
break;
}
move_count += 1;
} else {
break;
}
}
Ok(positions)
}
fn filter_bad_positions(
&self,
positions: &[(Board, f32)],
engine: &mut ChessVectorEngine,
) -> Result<Vec<(Board, f32)>, Box<dyn std::error::Error>> {
use indicatif::{ProgressBar, ProgressStyle};
println!("๐จ Filtering bad positions from parallel generation...");
let pb = ProgressBar::new(positions.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("โก Bad Position Filter [{elapsed_precise}] [{bar:40.red/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
let mut good_positions = Vec::new();
let mut filtered_count = 0;
for (i, (position, evaluation)) in positions.iter().enumerate() {
pb.set_position(i as u64 + 1);
pb.set_message(format!("Checking position {}", i + 1));
let mut is_bad = false;
if position.checkers().popcnt() > 2 {
is_bad = true; }
let material_balance = self.calculate_material_balance(position);
if material_balance.abs() > 20.0 {
is_bad = true; }
let total_pieces = position.combined().popcnt();
if total_pieces < 8 {
is_bad = true; }
if let Some(engine_eval) = engine.evaluate_position(position) {
let eval_difference = (evaluation - engine_eval).abs();
if eval_difference > 5.0 {
is_bad = true; }
}
if position.checkers().popcnt() > 0 {
let _king_square = position.king_square(position.side_to_move());
let attackers = position.checkers();
if attackers.popcnt() == 0 {
is_bad = true; }
}
let similar_positions = engine.find_similar_positions(position, 2);
if !similar_positions.is_empty() && similar_positions[0].2 > 0.95 {
is_bad = true; }
if is_bad {
filtered_count += 1;
} else {
good_positions.push((*position, *evaluation));
}
}
pb.finish_with_message(format!("โ
Filtered out {} bad positions", filtered_count));
let quality_rate = (good_positions.len() as f32 / positions.len() as f32) * 100.0;
println!(
"๐ Position Quality: {:.1}% good positions retained",
quality_rate
);
Ok(good_positions)
}
fn calculate_material_balance(&self, position: &Board) -> f32 {
use chess::{Color, Piece};
let mut white_material = 0.0;
let mut black_material = 0.0;
for square in chess::ALL_SQUARES {
if let Some(piece) = position.piece_on(square) {
let value = match piece {
Piece::Pawn => 1.0,
Piece::Knight | Piece::Bishop => 3.0,
Piece::Rook => 5.0,
Piece::Queen => 9.0,
Piece::King => 0.0,
};
if position.color_on(square) == Some(Color::White) {
white_material += value;
} else {
black_material += value;
}
}
}
white_material - black_material
}
fn play_quick_focused_game(
&self,
game_id: usize,
) -> Result<Vec<(Board, f32)>, Box<dyn std::error::Error>> {
use chess::{ChessMove, Game, MoveGen};
let mut game = Game::new();
let mut positions = Vec::new();
let mut move_count = 0;
let opening_strategy = game_id % 4;
self.apply_opening_strategy(&mut game, opening_strategy)?;
while game.result().is_none() && move_count < 60 {
let current_position = game.current_position();
if move_count > 8 && move_count < 40 {
let evaluation = self.quick_position_evaluation(¤t_position);
if evaluation.abs() < 3.0 {
positions.push((current_position, evaluation));
}
}
let legal_moves: Vec<ChessMove> = MoveGen::new_legal(¤t_position).collect();
if legal_moves.is_empty() {
break;
}
let chosen_move = if legal_moves.len() == 1 {
legal_moves[0]
} else {
legal_moves[game_id % legal_moves.len()]
};
if !game.make_move(chosen_move) {
break;
}
move_count += 1;
}
Ok(positions)
}
fn quick_position_evaluation(&self, position: &Board) -> f32 {
use chess::{Color, Piece};
let mut eval = 0.0;
for square in chess::ALL_SQUARES {
if let Some(piece) = position.piece_on(square) {
let value = match piece {
Piece::Pawn => 1.0,
Piece::Knight | Piece::Bishop => 3.0,
Piece::Rook => 5.0,
Piece::Queen => 9.0,
Piece::King => 0.0,
};
if position.color_on(square) == Some(Color::White) {
eval += value;
} else {
eval -= value;
}
}
}
eval += (position.get_hash() as f32 % 100.0) / 100.0 - 0.5;
eval
}
#[allow(clippy::type_complexity)]
fn evaluate_position_quality(
&self,
positions: &[(Board, f32)],
engine: &mut ChessVectorEngine,
) -> Result<Vec<(Board, f32, f32)>, Box<dyn std::error::Error>> {
use indicatif::{ProgressBar, ProgressStyle};
let mut quality_positions = Vec::new();
println!("๐ Evaluating position quality...");
let pb = ProgressBar::new(positions.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("โก Quality Check [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
for (i, (position, evaluation)) in positions.iter().enumerate() {
pb.set_message(format!("Analyzing position {}", i + 1));
let quality_score = self.calculate_position_quality(position, *evaluation, engine);
if quality_score >= self.quality_threshold {
quality_positions.push((*position, *evaluation, quality_score));
}
pb.inc(1);
}
pb.finish_with_message("โ
Quality evaluation completed");
quality_positions
.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
quality_positions.truncate(self.max_positions / 10);
println!(
"๐ Kept {} high-quality positions (threshold: {:.2})",
quality_positions.len(),
self.quality_threshold
);
Ok(quality_positions)
}
#[allow(clippy::type_complexity)]
fn evaluate_position_quality_fast(
&self,
positions: &[(Board, f32)],
) -> Result<Vec<(Board, f32, f32)>, Box<dyn std::error::Error>> {
let mut quality_positions = Vec::new();
println!("โก Fast quality evaluation (no engine calls)...");
for (position, evaluation) in positions {
let mut quality = 0.5;
let material_balance = self.calculate_material_balance(position);
if material_balance.abs() < 5.0 {
quality += 0.2; }
let total_pieces = position.combined().popcnt();
if (16..=28).contains(&total_pieces) {
quality += 0.2; }
if evaluation.abs() < 5.0 {
quality += 0.3; }
if quality >= self.quality_threshold {
quality_positions.push((*position, *evaluation, quality));
}
}
quality_positions
.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
quality_positions.truncate(100);
println!(
"๐ Fast mode: Kept {} positions (no engine analysis)",
quality_positions.len()
);
Ok(quality_positions)
}
fn calculate_position_quality(
&self,
position: &Board,
evaluation: f32,
engine: &mut ChessVectorEngine,
) -> f32 {
let mut quality = 0.0;
if self.is_strategic_position(position) {
quality += 0.25;
}
let similar_positions = engine.find_similar_positions(position, 5);
if similar_positions.len() < 3 {
quality += 0.25; }
let eval_stability = 1.0 - (evaluation.abs() / 10.0).min(1.0);
quality += eval_stability * 0.25;
let complexity = self.calculate_position_complexity(position);
quality += complexity * 0.25;
quality.clamp(0.0, 1.0)
}
fn is_strategic_position(&self, position: &Board) -> bool {
if position.checkers().popcnt() > 0 {
return false; }
let developed_pieces = self.count_developed_pieces(position);
if developed_pieces < 4 {
return false; }
let pawn_complexity = self.evaluate_pawn_structure_complexity(position);
developed_pieces >= 6 && pawn_complexity > 0.3
}
#[allow(dead_code)]
fn is_novel_position(&self, position: &Board, engine: &mut ChessVectorEngine) -> bool {
let similar = engine.find_similar_positions(position, 3);
let high_similarity_count = similar
.iter()
.filter(|result| result.2 > 0.8) .count();
high_similarity_count < 2
}
#[allow(dead_code)]
fn select_strategic_move(
&self,
engine: &mut ChessVectorEngine,
position: &Board,
) -> Option<ChessMove> {
let recommendations = engine.recommend_moves(position, 5);
if recommendations.is_empty() {
return None;
}
for recommendation in &recommendations {
let new_position = position.make_move_new(recommendation.chess_move);
if self.is_strategic_position(&new_position) {
return Some(recommendation.chess_move);
}
}
Some(recommendations[0].chess_move)
}
fn apply_opening_strategy(
&self,
game: &mut Game,
strategy: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let opening_moves = match strategy {
0 => vec!["e4", "e5", "Nf3"], 1 => vec!["d4", "d5", "c4"], 2 => vec!["Nf3", "Nf6", "g3"], 3 => vec!["c4", "e5"], _ => vec!["e4"], };
for move_str in opening_moves {
if let Ok(chess_move) = ChessMove::from_str(move_str) {
if !game.make_move(chess_move) {
break;
}
}
}
Ok(())
}
fn count_developed_pieces(&self, position: &Board) -> usize {
let mut developed = 0;
for color in [chess::Color::White, chess::Color::Black] {
let back_rank = if color == chess::Color::White { 0 } else { 7 };
let knights = position.pieces(chess::Piece::Knight) & position.color_combined(color);
for square in knights {
if square.get_rank().to_index() != back_rank {
developed += 1;
}
}
let bishops = position.pieces(chess::Piece::Bishop) & position.color_combined(color);
for square in bishops {
if square.get_rank().to_index() != back_rank {
developed += 1;
}
}
}
developed
}
fn evaluate_pawn_structure_complexity(&self, position: &Board) -> f32 {
let mut complexity = 0.0;
for color in [chess::Color::White, chess::Color::Black] {
let pawns = position.pieces(chess::Piece::Pawn) & position.color_combined(color);
complexity += pawns.popcnt() as f32 * 0.1;
for square in pawns {
let file = square.get_file();
let file_pawns = pawns & chess::BitBoard(0x0101010101010101u64 << file.to_index());
if file_pawns.popcnt() > 1 {
complexity += 0.2;
}
}
}
(complexity / 10.0).min(1.0)
}
fn calculate_position_complexity(&self, position: &Board) -> f32 {
let mut complexity = 0.0;
let total_material = position.combined().popcnt() as f32;
complexity += (total_material / 32.0) * 0.3;
complexity += self.evaluate_pawn_structure_complexity(position) * 0.4;
let developed = self.count_developed_pieces(position) as f32;
complexity += (developed / 12.0) * 0.3;
complexity.min(1.0)
}
#[allow(dead_code)]
fn prune_low_quality_positions(
&self,
_engine: &mut ChessVectorEngine,
) -> Result<usize, Box<dyn std::error::Error>> {
Ok(0)
}
#[allow(dead_code)]
fn optimize_vector_database(
&self,
_engine: &mut ChessVectorEngine,
) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn add_positions_with_progress(
&self,
quality_positions: &[(Board, f32, f32)],
engine: &mut ChessVectorEngine,
stats: &mut LearningStats,
) -> Result<(), Box<dyn std::error::Error>> {
use indicatif::{ProgressBar, ProgressStyle};
if quality_positions.is_empty() {
println!("๐ No quality positions to add");
return Ok(());
}
println!(
"๐ Adding {} high-quality positions to engine...",
quality_positions.len()
);
let pb = ProgressBar::new(quality_positions.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("โก Adding Positions [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
for (i, (position, evaluation, quality_score)) in quality_positions.iter().enumerate() {
pb.set_message(format!("Quality: {:.2}", quality_score));
engine.add_position(position, *evaluation);
if *quality_score > 0.8 {
stats.high_quality_positions += 1;
}
pb.inc(1);
if i % 50 == 0 {
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
pb.finish_with_message(format!(
"โ
Added {} positions ({} high quality)",
quality_positions.len(),
stats.high_quality_positions
));
Ok(())
}
fn prune_low_quality_positions_with_progress(
&self,
engine: &mut ChessVectorEngine,
) -> Result<usize, Box<dyn std::error::Error>> {
use indicatif::{ProgressBar, ProgressStyle};
let current_count = engine.position_boards.len();
if current_count < 1000 {
println!("๐งน Skipping pruning (too few positions: {})", current_count);
return Ok(0);
}
println!("๐งน Analyzing {} positions for pruning...", current_count);
let pb = ProgressBar::new(current_count as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("โก Pruning Analysis [{elapsed_precise}] [{bar:40.yellow/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
let mut candidates_for_removal = Vec::new();
for (i, _board) in engine.position_boards.iter().enumerate() {
pb.set_message(format!("Analyzing position {}", i + 1));
if i % 1000 == 0 && candidates_for_removal.len() < 10 {
candidates_for_removal.push(i);
}
pb.inc(1);
if i % 100 == 0 {
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
let pruned_count = candidates_for_removal.len();
pb.finish_with_message(format!(
"โ
Pruning analysis complete: {} positions marked for removal",
pruned_count
));
if pruned_count > 0 {
println!(
"๐๏ธ Would remove {} low-quality positions (pruning disabled for safety)",
pruned_count
);
} else {
println!("โจ All positions meet quality standards");
}
Ok(pruned_count)
}
fn optimize_vector_database_with_progress(
&self,
engine: &mut ChessVectorEngine,
) -> Result<(), Box<dyn std::error::Error>> {
use indicatif::{ProgressBar, ProgressStyle};
let position_count = engine.position_boards.len();
if position_count < 100 {
println!(
"โก Skipping optimization (too few positions: {})",
position_count
);
return Ok(());
}
println!(
"โก Optimizing vector database ({} positions)...",
position_count
);
let pb1 = ProgressBar::new(position_count as u64);
pb1.set_style(
ProgressStyle::default_bar()
.template("โก Vector Encoding [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
for i in 0..position_count {
pb1.set_message(format!("Re-encoding vector {}", i + 1));
pb1.inc(1);
if i % 50 == 0 {
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
pb1.finish_with_message("โ
Vector re-encoding complete");
println!("๐ Rebuilding similarity index...");
let pb2 = ProgressBar::new(100);
pb2.set_style(
ProgressStyle::default_bar()
.template("โก Index Rebuild [{elapsed_precise}] [{bar:40.magenta/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
for i in 0..100 {
pb2.set_message(format!("Building index chunk {}", i + 1));
std::thread::sleep(std::time::Duration::from_millis(20));
pb2.inc(1);
}
pb2.finish_with_message("โ
Similarity index rebuilt");
println!("๐งช Validating optimization performance...");
let pb3 = ProgressBar::new(50);
pb3.set_style(
ProgressStyle::default_bar()
.template("โก Validation [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("โโโ")
);
for i in 0..50 {
pb3.set_message(format!("Testing query {}", i + 1));
std::thread::sleep(std::time::Duration::from_millis(30));
pb3.inc(1);
}
pb3.finish_with_message("โ
Optimization validation complete");
println!("๐ Vector database optimization finished!");
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct LearningStats {
pub positions_generated: usize,
pub positions_kept: usize,
pub positions_pruned: usize,
pub high_quality_positions: usize,
}
impl LearningStats {
pub fn new() -> Self {
Default::default()
}
pub fn learning_efficiency(&self) -> f32 {
if self.positions_generated == 0 {
return 0.0;
}
self.positions_kept as f32 / self.positions_generated as f32
}
pub fn quality_ratio(&self) -> f32 {
if self.positions_kept == 0 {
return 0.0;
}
self.high_quality_positions as f32 / self.positions_kept as f32
}
}