1pub mod ann;
69pub mod auto_discovery;
70pub mod gpu_acceleration;
71pub mod lichess_loader;
72pub mod lsh;
73pub mod manifold_learner;
74pub mod nnue;
75pub mod opening_book;
76pub mod persistence;
77pub mod position_encoder;
78pub mod similarity_search;
79pub mod streaming_loader;
80pub mod tactical_search;
81pub mod training;
82pub mod ultra_fast_loader;
83pub mod variational_autoencoder;
84pub mod uci;
86
87pub use auto_discovery::{AutoDiscovery, FormatPriority, TrainingFile};
88pub use gpu_acceleration::{DeviceType, GPUAccelerator};
89pub use lichess_loader::LichessLoader;
90pub use lsh::LSH;
91pub use manifold_learner::ManifoldLearner;
92pub use nnue::{BlendStrategy, EvalStats, HybridEvaluator, NNUEConfig, NNUE};
93pub use opening_book::{OpeningBook, OpeningBookStats, OpeningEntry};
94pub use persistence::{Database, LSHTableData, PositionData};
95pub use position_encoder::PositionEncoder;
96pub use similarity_search::SimilaritySearch;
97pub use streaming_loader::StreamingLoader;
98pub use tactical_search::{TacticalConfig, TacticalResult, TacticalSearch};
99pub use training::{
100 AdvancedSelfLearningSystem, EngineEvaluator, GameExtractor, LearningProgress, LearningStats,
101 SelfPlayConfig, SelfPlayTrainer, TacticalPuzzle, TacticalPuzzleParser, TacticalTrainingData,
102 TrainingData, TrainingDataset,
103};
104pub use ultra_fast_loader::{LoadingStats, UltraFastLoader};
105pub use variational_autoencoder::{VAEConfig, VariationalAutoencoder};
106pub use uci::{run_uci_engine, run_uci_engine_with_config, UCIConfig, UCIEngine};
108
109use chess::{Board, ChessMove};
110use ndarray::{Array1, Array2};
111use serde_json::Value;
112use std::collections::HashMap;
113use std::path::Path;
114use std::str::FromStr;
115
116fn move_centrality(chess_move: &ChessMove) -> f32 {
119 let dest_square = chess_move.get_dest();
120 let rank = dest_square.get_rank().to_index() as f32;
121 let file = dest_square.get_file().to_index() as f32;
122
123 let center_rank = 3.5;
125 let center_file = 3.5;
126
127 let rank_distance = (rank - center_rank).abs();
128 let file_distance = (file - center_file).abs();
129
130 let max_distance = 3.5; let distance = (rank_distance + file_distance) / 2.0;
133 max_distance - distance
134}
135
136#[derive(Debug, Clone)]
138pub struct MoveRecommendation {
139 pub chess_move: ChessMove,
140 pub confidence: f32,
141 pub from_similar_position_count: usize,
142 pub average_outcome: f32,
143}
144
145#[derive(Debug, Clone)]
147pub struct TrainingStats {
148 pub total_positions: usize,
149 pub unique_positions: usize,
150 pub has_move_data: bool,
151 pub move_data_entries: usize,
152 pub lsh_enabled: bool,
153 pub manifold_enabled: bool,
154 pub opening_book_enabled: bool,
155}
156
157#[derive(Debug, Clone)]
159pub struct HybridConfig {
160 pub pattern_confidence_threshold: f32,
162 pub enable_tactical_refinement: bool,
164 pub tactical_config: TacticalConfig,
166 pub pattern_weight: f32,
168 pub min_similar_positions: usize,
170}
171
172impl Default for HybridConfig {
173 fn default() -> Self {
174 Self {
175 pattern_confidence_threshold: 0.85, enable_tactical_refinement: true,
177 tactical_config: TacticalConfig::default(),
178 pattern_weight: 0.3, min_similar_positions: 5, }
181 }
182}
183
184pub struct ChessVectorEngine {
238 encoder: PositionEncoder,
239 similarity_search: SimilaritySearch,
240 lsh_index: Option<LSH>,
241 manifold_learner: Option<ManifoldLearner>,
242 use_lsh: bool,
243 use_manifold: bool,
244 position_moves: HashMap<usize, Vec<(ChessMove, f32)>>,
246 manifold_similarity_search: Option<SimilaritySearch>,
248 manifold_lsh_index: Option<LSH>,
250 position_vectors: Vec<Array1<f32>>,
252 position_boards: Vec<Board>,
254 position_evaluations: Vec<f32>,
256 opening_book: Option<OpeningBook>,
258 database: Option<Database>,
260 tactical_search: Option<TacticalSearch>,
262 hybrid_config: HybridConfig,
266 nnue: Option<NNUE>,
268}
269
270impl Clone for ChessVectorEngine {
271 fn clone(&self) -> Self {
272 Self {
273 encoder: self.encoder.clone(),
274 similarity_search: self.similarity_search.clone(),
275 lsh_index: self.lsh_index.clone(),
276 manifold_learner: None, use_lsh: self.use_lsh,
278 use_manifold: false, position_moves: self.position_moves.clone(),
280 manifold_similarity_search: self.manifold_similarity_search.clone(),
281 manifold_lsh_index: self.manifold_lsh_index.clone(),
282 position_vectors: self.position_vectors.clone(),
283 position_boards: self.position_boards.clone(),
284 position_evaluations: self.position_evaluations.clone(),
285 opening_book: self.opening_book.clone(),
286 database: None, tactical_search: self.tactical_search.clone(),
288 hybrid_config: self.hybrid_config.clone(),
290 nnue: None, }
292 }
293}
294
295impl ChessVectorEngine {
296 pub fn new(vector_size: usize) -> Self {
298 let mut engine = Self {
299 encoder: PositionEncoder::new(vector_size),
300 similarity_search: SimilaritySearch::new(vector_size),
301 lsh_index: None,
302 manifold_learner: None,
303 use_lsh: false,
304 use_manifold: false,
305 position_moves: HashMap::new(),
306 manifold_similarity_search: None,
307 manifold_lsh_index: None,
308 position_vectors: Vec::new(),
309 position_boards: Vec::new(),
310 position_evaluations: Vec::new(),
311 opening_book: None,
312 database: None,
313 tactical_search: None,
314 hybrid_config: HybridConfig::default(),
316 nnue: None,
317 };
318
319 engine.enable_tactical_search_default();
321 engine
322 }
323
324 pub fn new_strong(vector_size: usize) -> Self {
326 let mut engine = Self::new(vector_size);
327 engine.enable_tactical_search(crate::tactical_search::TacticalConfig::strong());
329 engine
330 }
331
332 pub fn new_lightweight(vector_size: usize) -> Self {
334 Self {
335 encoder: PositionEncoder::new(vector_size),
336 similarity_search: SimilaritySearch::new(vector_size),
337 lsh_index: None,
338 manifold_learner: None,
339 use_lsh: false,
340 use_manifold: false,
341 position_moves: HashMap::new(),
342 manifold_similarity_search: None,
343 manifold_lsh_index: None,
344 position_vectors: Vec::new(),
345 position_boards: Vec::new(),
346 position_evaluations: Vec::new(),
347 opening_book: None,
348 database: None,
349 tactical_search: None, hybrid_config: HybridConfig::default(),
351 nnue: None,
352 }
353 }
354
355 pub fn new_adaptive(vector_size: usize, expected_positions: usize, use_case: &str) -> Self {
358 match use_case {
359 "training" => {
360 if expected_positions > 10000 {
361 Self::new_with_lsh(vector_size, 12, 20)
363 } else {
364 Self::new(vector_size)
365 }
366 }
367 "gameplay" => {
368 if expected_positions > 15000 {
369 Self::new_with_lsh(vector_size, 10, 18)
371 } else {
372 Self::new(vector_size)
373 }
374 }
375 "analysis" => {
376 if expected_positions > 10000 {
377 Self::new_with_lsh(vector_size, 14, 22)
379 } else {
380 Self::new(vector_size)
381 }
382 }
383 _ => Self::new(vector_size), }
385 }
386
387 pub fn new_with_lsh(vector_size: usize, num_tables: usize, hash_size: usize) -> Self {
389 Self {
390 encoder: PositionEncoder::new(vector_size),
391 similarity_search: SimilaritySearch::new(vector_size),
392 lsh_index: Some(LSH::new(vector_size, num_tables, hash_size)),
393 manifold_learner: None,
394 use_lsh: true,
395 use_manifold: false,
396 position_moves: HashMap::new(),
397 manifold_similarity_search: None,
398 manifold_lsh_index: None,
399 position_vectors: Vec::new(),
400 position_boards: Vec::new(),
401 position_evaluations: Vec::new(),
402 opening_book: None,
403 database: None,
404 tactical_search: None,
405 hybrid_config: HybridConfig::default(),
407 nnue: None,
408 }
409 }
410
411 pub fn enable_lsh(&mut self, num_tables: usize, hash_size: usize) {
413 self.lsh_index = Some(LSH::new(self.encoder.vector_size(), num_tables, hash_size));
414 self.use_lsh = true;
415
416 if let Some(ref mut lsh) = self.lsh_index {
418 for (vector, evaluation) in self.similarity_search.get_all_positions() {
419 lsh.add_vector(vector, evaluation);
420 }
421 }
422 }
423
424 pub fn add_position(&mut self, board: &Board, evaluation: f32) {
426 if !self.is_position_safe(board) {
428 return; }
430
431 let vector = self.encoder.encode(board);
432 self.similarity_search
433 .add_position(vector.clone(), evaluation);
434
435 self.position_vectors.push(vector.clone());
437 self.position_boards.push(*board);
438 self.position_evaluations.push(evaluation);
439
440 if let Some(ref mut lsh) = self.lsh_index {
442 lsh.add_vector(vector.clone(), evaluation);
443 }
444
445 if self.use_manifold {
447 if let Some(ref learner) = self.manifold_learner {
448 let compressed = learner.encode(&vector);
449
450 if let Some(ref mut search) = self.manifold_similarity_search {
451 search.add_position(compressed.clone(), evaluation);
452 }
453
454 if let Some(ref mut lsh) = self.manifold_lsh_index {
455 lsh.add_vector(compressed, evaluation);
456 }
457 }
458 }
459 }
460
461 pub fn find_similar_positions(&self, board: &Board, k: usize) -> Vec<(Array1<f32>, f32, f32)> {
463 let query_vector = self.encoder.encode(board);
464
465 if self.use_manifold {
467 if let Some(ref manifold_learner) = self.manifold_learner {
468 let compressed_query = manifold_learner.encode(&query_vector);
469
470 if let Some(ref lsh) = self.manifold_lsh_index {
472 return lsh.query(&compressed_query, k);
473 }
474
475 if let Some(ref search) = self.manifold_similarity_search {
477 return search.search(&compressed_query, k);
478 }
479 }
480 }
481
482 if self.use_lsh {
484 if let Some(ref lsh_index) = self.lsh_index {
485 return lsh_index.query(&query_vector, k);
486 }
487 }
488
489 self.similarity_search.search(&query_vector, k)
491 }
492
493 pub fn find_similar_positions_with_indices(
495 &self,
496 board: &Board,
497 k: usize,
498 ) -> Vec<(usize, f32, f32)> {
499 let query_vector = self.encoder.encode(board);
500
501 let mut results = Vec::new();
504
505 for (i, stored_vector) in self.position_vectors.iter().enumerate() {
506 let similarity = self.encoder.similarity(&query_vector, stored_vector);
507 let eval = self.position_evaluations.get(i).copied().unwrap_or(0.0);
508 results.push((i, eval, similarity));
509 }
510
511 results.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
513 results.truncate(k);
514
515 results
516 }
517
518 pub fn evaluate_position(&mut self, board: &Board) -> Option<f32> {
520 if let Some(entry) = self.get_opening_entry(board) {
529 return Some(entry.evaluation);
530 }
531
532 let nnue_evaluation = if let Some(ref mut nnue) = self.nnue {
534 nnue.evaluate(board).ok()
535 } else {
536 None
537 };
538
539 let similar_positions = self.find_similar_positions(board, 5);
541
542 if similar_positions.is_empty() {
543 if let Some(nnue_eval) = nnue_evaluation {
545 return Some(nnue_eval);
546 }
547
548 if let Some(ref mut tactical_search) = self.tactical_search {
549 let result = tactical_search.search(board);
550 return Some(result.evaluation);
551 }
552 return None;
553 }
554
555 let mut weighted_sum = 0.0;
557 let mut weight_sum = 0.0;
558 let mut similarity_scores = Vec::new();
559
560 for (_, evaluation, similarity) in &similar_positions {
561 let weight = *similarity;
562 weighted_sum += evaluation * weight;
563 weight_sum += weight;
564 similarity_scores.push(*similarity);
565 }
566
567 let pattern_evaluation = weighted_sum / weight_sum;
568
569 let avg_similarity = similarity_scores.iter().sum::<f32>() / similarity_scores.len() as f32;
571 let count_factor = (similar_positions.len() as f32
572 / self.hybrid_config.min_similar_positions as f32)
573 .min(1.0);
574 let pattern_confidence = avg_similarity * count_factor;
575
576 let use_tactical = self.hybrid_config.enable_tactical_refinement
578 && pattern_confidence < self.hybrid_config.pattern_confidence_threshold
579 && self.tactical_search.is_some();
580
581 if use_tactical {
582 if let Some(ref mut tactical_search) = self.tactical_search {
584 let tactical_result = if tactical_search.config.enable_parallel_search {
585 tactical_search.search_parallel(board)
586 } else {
587 tactical_search.search(board)
588 };
589
590 let mut hybrid_evaluation = pattern_evaluation;
592
593 if nnue_evaluation.is_some() {
595 if let Some(ref mut nnue) = self.nnue {
597 if let Ok(nnue_hybrid_eval) =
598 nnue.evaluate_hybrid(board, Some(pattern_evaluation))
599 {
600 hybrid_evaluation = nnue_hybrid_eval;
601 }
602 }
603 }
604
605 let pattern_weight = self.hybrid_config.pattern_weight * pattern_confidence;
607 let tactical_weight = 1.0 - pattern_weight;
608
609 hybrid_evaluation = (hybrid_evaluation * pattern_weight)
610 + (tactical_result.evaluation * tactical_weight);
611
612 Some(hybrid_evaluation)
613 } else {
614 if nnue_evaluation.is_some() {
616 if let Some(ref mut nnue) = self.nnue {
617 nnue.evaluate_hybrid(board, Some(pattern_evaluation)).ok()
619 } else {
620 Some(pattern_evaluation)
621 }
622 } else {
623 Some(pattern_evaluation)
624 }
625 }
626 } else {
627 if nnue_evaluation.is_some() {
629 if let Some(ref mut nnue) = self.nnue {
630 nnue.evaluate_hybrid(board, Some(pattern_evaluation)).ok()
632 } else {
633 Some(pattern_evaluation)
634 }
635 } else {
636 Some(pattern_evaluation)
637 }
638 }
639 }
640
641 pub fn encode_position(&self, board: &Board) -> Array1<f32> {
643 self.encoder.encode(board)
644 }
645
646 pub fn calculate_similarity(&self, board1: &Board, board2: &Board) -> f32 {
648 let vec1 = self.encoder.encode(board1);
649 let vec2 = self.encoder.encode(board2);
650 self.encoder.similarity(&vec1, &vec2)
651 }
652
653 pub fn knowledge_base_size(&self) -> usize {
655 self.similarity_search.size()
656 }
657
658 pub fn save_training_data<P: AsRef<std::path::Path>>(
660 &self,
661 path: P,
662 ) -> Result<(), Box<dyn std::error::Error>> {
663 use crate::training::{TrainingData, TrainingDataset};
664
665 let mut dataset = TrainingDataset::new();
666
667 for (i, board) in self.position_boards.iter().enumerate() {
669 if i < self.position_evaluations.len() {
670 dataset.data.push(TrainingData {
671 board: *board,
672 evaluation: self.position_evaluations[i],
673 depth: 15, game_id: i, });
676 }
677 }
678
679 dataset.save_incremental(path)?;
680 println!("Saved {} positions to training data", dataset.data.len());
681 Ok(())
682 }
683
684 pub fn load_training_data_incremental<P: AsRef<std::path::Path>>(
686 &mut self,
687 path: P,
688 ) -> Result<(), Box<dyn std::error::Error>> {
689 use crate::training::TrainingDataset;
690 use indicatif::{ProgressBar, ProgressStyle};
691 use std::collections::HashSet;
692
693 let existing_size = self.knowledge_base_size();
694
695 let path_ref = path.as_ref();
697 let binary_path = path_ref.with_extension("bin");
698 if binary_path.exists() {
699 println!("š Loading optimized binary format...");
700 return self.load_training_data_binary(binary_path);
701 }
702
703 println!("š Loading training data from {}...", path_ref.display());
704 let dataset = TrainingDataset::load(path)?;
705
706 let total_positions = dataset.data.len();
707 if total_positions == 0 {
708 println!("ā ļø No positions found in dataset");
709 return Ok(());
710 }
711
712 let dedup_pb = ProgressBar::new(total_positions as u64);
714 dedup_pb.set_style(
715 ProgressStyle::default_bar()
716 .template("š Checking duplicates [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")?
717 .progress_chars("āāā")
718 );
719
720 let mut existing_boards: HashSet<_> = self.position_boards.iter().cloned().collect();
722 let mut new_positions = Vec::new();
723 let mut new_evaluations = Vec::new();
724
725 for (i, data) in dataset.data.into_iter().enumerate() {
727 if !existing_boards.contains(&data.board) {
728 existing_boards.insert(data.board);
729 new_positions.push(data.board);
730 new_evaluations.push(data.evaluation);
731 }
732
733 if i % 1000 == 0 || i == total_positions - 1 {
734 dedup_pb.set_position((i + 1) as u64);
735 dedup_pb.set_message(format!("{} new positions found", new_positions.len()));
736 }
737 }
738 dedup_pb.finish_with_message(format!("ā
Found {} new positions", new_positions.len()));
739
740 if new_positions.is_empty() {
741 println!("ā¹ļø No new positions to add (all positions already exist)");
742 return Ok(());
743 }
744
745 let add_pb = ProgressBar::new(new_positions.len() as u64);
747 add_pb.set_style(
748 ProgressStyle::default_bar()
749 .template("ā Adding positions [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")?
750 .progress_chars("āāā")
751 );
752
753 for (i, (board, evaluation)) in new_positions
755 .into_iter()
756 .zip(new_evaluations.into_iter())
757 .enumerate()
758 {
759 self.add_position(&board, evaluation);
760
761 if i % 500 == 0 || i == add_pb.length().unwrap() as usize - 1 {
762 add_pb.set_position((i + 1) as u64);
763 add_pb.set_message("vectors encoded".to_string());
764 }
765 }
766 add_pb.finish_with_message("ā
All positions added");
767
768 println!(
769 "šÆ Loaded {} new positions (total: {})",
770 self.knowledge_base_size() - existing_size,
771 self.knowledge_base_size()
772 );
773 Ok(())
774 }
775
776 pub fn save_training_data_binary<P: AsRef<std::path::Path>>(
778 &self,
779 path: P,
780 ) -> Result<(), Box<dyn std::error::Error>> {
781 use lz4_flex::compress_prepend_size;
782
783 println!("š¾ Saving training data in binary format (compressed)...");
784
785 #[derive(serde::Serialize)]
787 struct BinaryTrainingData {
788 positions: Vec<String>, evaluations: Vec<f32>,
790 vectors: Vec<Vec<f32>>, created_at: i64,
792 }
793
794 let current_time = std::time::SystemTime::now()
795 .duration_since(std::time::UNIX_EPOCH)?
796 .as_secs() as i64;
797
798 let mut positions = Vec::with_capacity(self.position_boards.len());
800 let mut evaluations = Vec::with_capacity(self.position_boards.len());
801 let mut vectors = Vec::with_capacity(self.position_boards.len());
802
803 for (i, board) in self.position_boards.iter().enumerate() {
804 if i < self.position_evaluations.len() {
805 positions.push(board.to_string());
806 evaluations.push(self.position_evaluations[i]);
807
808 if i < self.position_vectors.len() {
810 if let Some(vector_slice) = self.position_vectors[i].as_slice() {
811 vectors.push(vector_slice.to_vec());
812 }
813 }
814 }
815 }
816
817 let binary_data = BinaryTrainingData {
818 positions,
819 evaluations,
820 vectors,
821 created_at: current_time,
822 };
823
824 let serialized = bincode::serialize(&binary_data)?;
826
827 let compressed = compress_prepend_size(&serialized);
829
830 std::fs::write(path, &compressed)?;
832
833 println!(
834 "ā
Saved {} positions to binary file ({} bytes compressed)",
835 binary_data.positions.len(),
836 compressed.len()
837 );
838 Ok(())
839 }
840
841 pub fn load_training_data_binary<P: AsRef<std::path::Path>>(
843 &mut self,
844 path: P,
845 ) -> Result<(), Box<dyn std::error::Error>> {
846 use indicatif::{ProgressBar, ProgressStyle};
847 use lz4_flex::decompress_size_prepended;
848 use rayon::prelude::*;
849
850 println!("š Loading training data from binary format...");
851
852 #[derive(serde::Deserialize)]
853 struct BinaryTrainingData {
854 positions: Vec<String>,
855 evaluations: Vec<f32>,
856 #[allow(dead_code)]
857 vectors: Vec<Vec<f32>>,
858 #[allow(dead_code)]
859 created_at: i64,
860 }
861
862 let existing_size = self.knowledge_base_size();
863
864 let file_size = std::fs::metadata(&path)?.len();
866 println!(
867 "š¦ Reading {} compressed file...",
868 Self::format_bytes(file_size)
869 );
870
871 let compressed_data = std::fs::read(path)?;
872 println!("š Decompressing data...");
873 let serialized = decompress_size_prepended(&compressed_data)?;
874
875 println!("š Deserializing binary data...");
876 let binary_data: BinaryTrainingData = bincode::deserialize(&serialized)?;
877
878 let total_positions = binary_data.positions.len();
879 if total_positions == 0 {
880 println!("ā ļø No positions found in binary file");
881 return Ok(());
882 }
883
884 println!("š Processing {total_positions} positions from binary format...");
885
886 let pb = ProgressBar::new(total_positions as u64);
888 pb.set_style(
889 ProgressStyle::default_bar()
890 .template("ā” Loading positions [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")?
891 .progress_chars("āāā")
892 );
893
894 let mut added_count = 0;
895
896 if total_positions > 10_000 {
898 println!("š Using parallel batch processing for large dataset...");
899
900 let existing_positions: std::collections::HashSet<_> =
902 self.position_boards.iter().cloned().collect();
903
904 let batch_size = 5000.min(total_positions / num_cpus::get()).max(1000);
906 let batches: Vec<_> = binary_data
907 .positions
908 .chunks(batch_size)
909 .zip(binary_data.evaluations.chunks(batch_size))
910 .collect();
911
912 println!(
913 "š Processing {} batches of ~{} positions each...",
914 batches.len(),
915 batch_size
916 );
917
918 let valid_positions: Vec<Vec<(Board, f32)>> = batches
920 .par_iter()
921 .map(|(fen_batch, eval_batch)| {
922 let mut batch_positions = Vec::new();
923
924 for (fen, &evaluation) in fen_batch.iter().zip(eval_batch.iter()) {
925 if let Ok(board) = fen.parse::<Board>() {
926 if !existing_positions.contains(&board) {
927 let mut eval = evaluation;
928 if eval.abs() > 15.0 {
930 eval /= 100.0;
931 }
932 batch_positions.push((board, eval));
933 }
934 }
935 }
936
937 batch_positions
938 })
939 .collect();
940
941 for batch in valid_positions {
943 for (board, evaluation) in batch {
944 self.add_position(&board, evaluation);
945 added_count += 1;
946
947 if added_count % 1000 == 0 {
948 pb.set_position(added_count as u64);
949 pb.set_message(format!("{added_count} new positions"));
950 }
951 }
952 }
953 } else {
954 for (i, fen) in binary_data.positions.iter().enumerate() {
956 if i < binary_data.evaluations.len() {
957 if let Ok(board) = fen.parse() {
958 if !self.position_boards.contains(&board) {
960 let mut evaluation = binary_data.evaluations[i];
961
962 if evaluation.abs() > 15.0 {
964 evaluation /= 100.0;
965 }
966
967 self.add_position(&board, evaluation);
968 added_count += 1;
969 }
970 }
971 }
972
973 if i % 1000 == 0 || i == total_positions - 1 {
974 pb.set_position((i + 1) as u64);
975 pb.set_message(format!("{added_count} new positions"));
976 }
977 }
978 }
979 pb.finish_with_message(format!("ā
Loaded {added_count} new positions"));
980
981 println!(
982 "šÆ Binary loading complete: {} new positions (total: {})",
983 self.knowledge_base_size() - existing_size,
984 self.knowledge_base_size()
985 );
986 Ok(())
987 }
988
989 pub fn load_training_data_mmap<P: AsRef<Path>>(
992 &mut self,
993 path: P,
994 ) -> Result<(), Box<dyn std::error::Error>> {
995 use memmap2::Mmap;
996 use std::fs::File;
997
998 let path_ref = path.as_ref();
999 println!(
1000 "š Loading training data via memory mapping: {}",
1001 path_ref.display()
1002 );
1003
1004 let file = File::open(path_ref)?;
1005 let mmap = unsafe { Mmap::map(&file)? };
1006
1007 if let Ok(data) = rmp_serde::from_slice::<Vec<(String, f32)>>(&mmap) {
1009 println!("š¦ Detected MessagePack format");
1010 return self.load_positions_from_tuples(data);
1011 }
1012
1013 if let Ok(data) = bincode::deserialize::<Vec<(String, f32)>>(&mmap) {
1015 println!("š¦ Detected bincode format");
1016 return self.load_positions_from_tuples(data);
1017 }
1018
1019 let decompressed = lz4_flex::decompress_size_prepended(&mmap)?;
1021 let data: Vec<(String, f32)> = bincode::deserialize(&decompressed)?;
1022 println!("š¦ Detected LZ4+bincode format");
1023 self.load_positions_from_tuples(data)
1024 }
1025
1026 pub fn load_training_data_msgpack<P: AsRef<Path>>(
1029 &mut self,
1030 path: P,
1031 ) -> Result<(), Box<dyn std::error::Error>> {
1032 use std::fs::File;
1033 use std::io::BufReader;
1034
1035 let path_ref = path.as_ref();
1036 println!(
1037 "š Loading MessagePack training data: {}",
1038 path_ref.display()
1039 );
1040
1041 let file = File::open(path_ref)?;
1042 let reader = BufReader::new(file);
1043 let data: Vec<(String, f32)> = rmp_serde::from_read(reader)?;
1044
1045 println!("š¦ MessagePack data loaded: {} positions", data.len());
1046 self.load_positions_from_tuples(data)
1047 }
1048
1049 pub fn load_training_data_streaming_json<P: AsRef<Path>>(
1052 &mut self,
1053 path: P,
1054 ) -> Result<(), Box<dyn std::error::Error>> {
1055 use dashmap::DashMap;
1056 use rayon::prelude::*;
1057 use std::fs::File;
1058 use std::io::{BufRead, BufReader};
1059 use std::sync::Arc;
1060
1061 let path_ref = path.as_ref();
1062 println!(
1063 "š Loading JSON with streaming parallel processing: {}",
1064 path_ref.display()
1065 );
1066
1067 let file = File::open(path_ref)?;
1068 let reader = BufReader::new(file);
1069
1070 let chunk_size = 10000;
1072 let position_map = Arc::new(DashMap::new());
1073
1074 let lines: Vec<String> = reader.lines().collect::<Result<Vec<_>, _>>()?;
1075 let total_lines = lines.len();
1076
1077 lines.par_chunks(chunk_size).for_each(|chunk| {
1079 for line in chunk {
1080 if let Ok(data) = serde_json::from_str::<serde_json::Value>(line) {
1081 if let (Some(fen), Some(eval)) = (
1082 data.get("fen").and_then(|v| v.as_str()),
1083 data.get("evaluation").and_then(|v| v.as_f64()),
1084 ) {
1085 position_map.insert(fen.to_string(), eval as f32);
1086 }
1087 }
1088 }
1089 });
1090
1091 println!(
1092 "š¦ Parallel JSON processing complete: {} positions from {} lines",
1093 position_map.len(),
1094 total_lines
1095 );
1096
1097 let data: Vec<(String, f32)> = match Arc::try_unwrap(position_map) {
1100 Ok(map) => map.into_iter().collect(),
1101 Err(arc_map) => {
1102 arc_map
1104 .iter()
1105 .map(|entry| (entry.key().clone(), *entry.value()))
1106 .collect()
1107 }
1108 };
1109 self.load_positions_from_tuples(data)
1110 }
1111
1112 pub fn load_training_data_compressed<P: AsRef<Path>>(
1115 &mut self,
1116 path: P,
1117 ) -> Result<(), Box<dyn std::error::Error>> {
1118 use std::fs::File;
1119 use std::io::BufReader;
1120
1121 let path_ref = path.as_ref();
1122 println!(
1123 "š Loading zstd compressed training data: {}",
1124 path_ref.display()
1125 );
1126
1127 let file = File::open(path_ref)?;
1128 let reader = BufReader::new(file);
1129 let decoder = zstd::stream::Decoder::new(reader)?;
1130
1131 if let Ok(data) = rmp_serde::from_read::<_, Vec<(String, f32)>>(decoder) {
1133 println!("š¦ Zstd+MessagePack data loaded: {} positions", data.len());
1134 return self.load_positions_from_tuples(data);
1135 }
1136
1137 let file = File::open(path_ref)?;
1139 let reader = BufReader::new(file);
1140 let decoder = zstd::stream::Decoder::new(reader)?;
1141 let data: Vec<(String, f32)> = bincode::deserialize_from(decoder)?;
1142
1143 println!("š¦ Zstd+bincode data loaded: {} positions", data.len());
1144 self.load_positions_from_tuples(data)
1145 }
1146
1147 fn load_positions_from_tuples(
1150 &mut self,
1151 data: Vec<(String, f32)>,
1152 ) -> Result<(), Box<dyn std::error::Error>> {
1153 use indicatif::{ProgressBar, ProgressStyle};
1154 use std::collections::HashSet;
1155
1156 let existing_size = self.knowledge_base_size();
1157 let mut seen_positions = HashSet::new();
1158 let mut loaded_count = 0;
1159
1160 let pb = ProgressBar::new(data.len() as u64);
1162 pb.set_style(ProgressStyle::with_template(
1163 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({per_sec}) {msg}"
1164 )?);
1165
1166 for (fen, evaluation) in data {
1167 pb.inc(1);
1168
1169 if seen_positions.contains(&fen) {
1171 continue;
1172 }
1173 seen_positions.insert(fen.clone());
1174
1175 if let Ok(board) = Board::from_str(&fen) {
1177 self.add_position(&board, evaluation);
1178 loaded_count += 1;
1179
1180 if loaded_count % 1000 == 0 {
1181 pb.set_message(format!("Loaded {loaded_count} positions"));
1182 }
1183 }
1184 }
1185
1186 pb.finish_with_message(format!("ā
Loaded {loaded_count} new positions"));
1187
1188 println!(
1189 "šÆ Ultra-fast loading complete: {} new positions (total: {})",
1190 self.knowledge_base_size() - existing_size,
1191 self.knowledge_base_size()
1192 );
1193
1194 Ok(())
1195 }
1196
1197 fn format_bytes(bytes: u64) -> String {
1199 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
1200 let mut size = bytes as f64;
1201 let mut unit_index = 0;
1202
1203 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
1204 size /= 1024.0;
1205 unit_index += 1;
1206 }
1207
1208 format!("{:.1} {}", size, UNITS[unit_index])
1209 }
1210
1211 pub fn train_from_dataset_incremental(&mut self, dataset: &crate::training::TrainingDataset) {
1213 let _existing_size = self.knowledge_base_size();
1214 let mut added = 0;
1215
1216 for data in &dataset.data {
1217 if !self.position_boards.contains(&data.board) {
1219 self.add_position(&data.board, data.evaluation);
1220 added += 1;
1221 }
1222 }
1223
1224 println!(
1225 "Added {} new positions from dataset (total: {})",
1226 added,
1227 self.knowledge_base_size()
1228 );
1229 }
1230
1231 pub fn training_stats(&self) -> TrainingStats {
1233 TrainingStats {
1234 total_positions: self.knowledge_base_size(),
1235 unique_positions: self.position_boards.len(),
1236 has_move_data: !self.position_moves.is_empty(),
1237 move_data_entries: self.position_moves.len(),
1238 lsh_enabled: self.use_lsh,
1239 manifold_enabled: self.use_manifold,
1240 opening_book_enabled: self.opening_book.is_some(),
1241 }
1242 }
1243
1244 pub fn auto_load_training_data(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
1246 use indicatif::{ProgressBar, ProgressStyle};
1247
1248 let common_files = vec![
1249 "training_data.json",
1250 "tactical_training_data.json",
1251 "engine_training.json",
1252 "chess_training.json",
1253 "my_training.json",
1254 ];
1255
1256 let tactical_files = vec![
1257 "tactical_puzzles.json",
1258 "lichess_puzzles.json",
1259 "my_puzzles.json",
1260 ];
1261
1262 let mut available_files = Vec::new();
1264 for file_path in &common_files {
1265 if std::path::Path::new(file_path).exists() {
1266 available_files.push((file_path, "training"));
1267 }
1268 }
1269 for file_path in &tactical_files {
1270 if std::path::Path::new(file_path).exists() {
1271 available_files.push((file_path, "tactical"));
1272 }
1273 }
1274
1275 if available_files.is_empty() {
1276 return Ok(Vec::new());
1277 }
1278
1279 println!(
1280 "š Found {} training files to auto-load",
1281 available_files.len()
1282 );
1283
1284 let pb = ProgressBar::new(available_files.len() as u64);
1286 pb.set_style(
1287 ProgressStyle::default_bar()
1288 .template("š Auto-loading files [{elapsed_precise}] [{bar:40.blue/cyan}] {pos}/{len} {msg}")?
1289 .progress_chars("āāā")
1290 );
1291
1292 let mut loaded_files = Vec::new();
1293
1294 for (i, (file_path, file_type)) in available_files.iter().enumerate() {
1295 pb.set_position(i as u64);
1296 pb.set_message("Processing...".to_string());
1297
1298 let result = match *file_type {
1299 "training" => self.load_training_data_incremental(file_path).map(|_| {
1300 loaded_files.push(file_path.to_string());
1301 println!("Loading complete");
1302 }),
1303 "tactical" => crate::training::TacticalPuzzleParser::load_tactical_puzzles(
1304 file_path,
1305 )
1306 .map(|puzzles| {
1307 crate::training::TacticalPuzzleParser::load_into_engine_incremental(
1308 &puzzles, self,
1309 );
1310 loaded_files.push(file_path.to_string());
1311 println!("Loading complete");
1312 }),
1313 _ => Ok(()),
1314 };
1315
1316 if let Err(_e) = result {
1317 println!("Loading complete");
1318 }
1319 }
1320
1321 pb.set_position(available_files.len() as u64);
1322 pb.finish_with_message(format!("ā
Auto-loaded {} files", loaded_files.len()));
1323
1324 Ok(loaded_files)
1325 }
1326
1327 pub fn load_lichess_puzzles<P: AsRef<std::path::Path>>(
1329 &mut self,
1330 csv_path: P,
1331 ) -> Result<(), Box<dyn std::error::Error>> {
1332 println!("š„ Loading Lichess puzzles with enhanced performance...");
1333 let puzzle_entries =
1334 crate::lichess_loader::load_lichess_puzzles_basic_with_moves(csv_path, 100000)?;
1335
1336 for (board, evaluation, best_move) in puzzle_entries {
1337 self.add_position_with_move(&board, evaluation, Some(best_move), Some(evaluation));
1338 }
1339
1340 println!("ā
Lichess puzzle loading complete!");
1341 Ok(())
1342 }
1343
1344 pub fn load_lichess_puzzles_with_limit<P: AsRef<std::path::Path>>(
1346 &mut self,
1347 csv_path: P,
1348 max_puzzles: Option<usize>,
1349 ) -> Result<(), Box<dyn std::error::Error>> {
1350 match max_puzzles {
1351 Some(limit) => {
1352 println!("š Loading Lichess puzzles (limited to {limit} puzzles)...");
1353 let puzzle_entries =
1354 crate::lichess_loader::load_lichess_puzzles_basic_with_moves(csv_path, limit)?;
1355
1356 for (board, evaluation, best_move) in puzzle_entries {
1357 self.add_position_with_move(
1358 &board,
1359 evaluation,
1360 Some(best_move),
1361 Some(evaluation),
1362 );
1363 }
1364 }
1365 None => {
1366 self.load_lichess_puzzles(csv_path)?;
1368 return Ok(());
1369 }
1370 }
1371
1372 println!("ā
Lichess puzzle loading complete!");
1373 Ok(())
1374 }
1375
1376 pub fn new_with_auto_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1378 let mut engine = Self::new(vector_size);
1379 engine.enable_opening_book();
1380
1381 let loaded_files = engine.auto_load_training_data()?;
1383
1384 if loaded_files.is_empty() {
1385 println!("š¤ Created fresh engine (no training data found)");
1386 } else {
1387 println!(
1388 "š Created engine with auto-loaded training data from {} files",
1389 loaded_files.len()
1390 );
1391 let _stats = engine.training_stats();
1392 println!("Loading complete");
1393 println!("Loading complete");
1394 }
1395
1396 Ok(engine)
1397 }
1398
1399 pub fn new_with_fast_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1402 use indicatif::{ProgressBar, ProgressStyle};
1403
1404 let mut engine = Self::new(vector_size);
1405 engine.enable_opening_book();
1406
1407 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
1409 println!("Loading complete");
1410 }
1411
1412 let binary_files = [
1414 "training_data_a100.bin", "training_data.bin",
1416 "tactical_training_data.bin",
1417 "engine_training.bin",
1418 "chess_training.bin",
1419 ];
1420
1421 let existing_binary_files: Vec<_> = binary_files
1423 .iter()
1424 .filter(|&file_path| std::path::Path::new(file_path).exists())
1425 .collect();
1426
1427 let mut loaded_count = 0;
1428
1429 if !existing_binary_files.is_empty() {
1430 println!(
1431 "ā” Fast loading: Found {} binary files",
1432 existing_binary_files.len()
1433 );
1434
1435 let pb = ProgressBar::new(existing_binary_files.len() as u64);
1437 pb.set_style(
1438 ProgressStyle::default_bar()
1439 .template("š Fast loading [{elapsed_precise}] [{bar:40.green/cyan}] {pos}/{len} {msg}")?
1440 .progress_chars("āāā")
1441 );
1442
1443 for (i, file_path) in existing_binary_files.iter().enumerate() {
1444 pb.set_position(i as u64);
1445 pb.set_message("Processing...".to_string());
1446
1447 if engine.load_training_data_binary(file_path).is_ok() {
1448 loaded_count += 1;
1449 }
1450 }
1451
1452 pb.set_position(existing_binary_files.len() as u64);
1453 pb.finish_with_message(format!("ā
Loaded {loaded_count} binary files"));
1454 } else {
1455 println!("š¦ No binary files found, falling back to JSON auto-loading...");
1456 let _ = engine.auto_load_training_data()?;
1457 }
1458
1459 if let Err(e) = engine.load_manifold_models() {
1461 println!("ā ļø No pre-trained manifold models found ({e})");
1462 println!(" Use --rebuild-models flag to train new models");
1463 }
1464
1465 let stats = engine.training_stats();
1466 println!(
1467 "ā” Fast engine ready with {} positions ({} binary files loaded)",
1468 stats.total_positions, loaded_count
1469 );
1470
1471 Ok(engine)
1472 }
1473
1474 pub fn new_with_auto_discovery(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1477 println!("š Initializing engine with AUTO-DISCOVERY and format consolidation...");
1478 let mut engine = Self::new(vector_size);
1479 engine.enable_opening_book();
1480
1481 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
1483 println!("Loading complete");
1484 }
1485
1486 let discovered_files = AutoDiscovery::discover_training_files(".", true)?;
1488
1489 if discovered_files.is_empty() {
1490 println!("ā¹ļø No training data found. Use convert methods to create optimized files.");
1491 return Ok(engine);
1492 }
1493
1494 let consolidated = AutoDiscovery::consolidate_by_base_name(discovered_files.clone());
1496
1497 let mut total_loaded = 0;
1498 for (base_name, best_file) in &consolidated {
1499 println!("š Loading {} ({})", base_name, best_file.format);
1500
1501 let initial_size = engine.knowledge_base_size();
1502 engine.load_file_by_format(&best_file.path, &best_file.format)?;
1503 let loaded_count = engine.knowledge_base_size() - initial_size;
1504 total_loaded += loaded_count;
1505
1506 println!(" ā
Loaded {loaded_count} positions");
1507 }
1508
1509 let cleanup_candidates = AutoDiscovery::get_cleanup_candidates(&discovered_files);
1511 if !cleanup_candidates.is_empty() {
1512 println!(
1513 "š§¹ Found {} old format files that can be cleaned up:",
1514 cleanup_candidates.len()
1515 );
1516 AutoDiscovery::cleanup_old_formats(&cleanup_candidates, true)?; println!(" š” To actually remove old files, run: cargo run --bin cleanup_formats");
1519 }
1520
1521 if let Err(e) = engine.load_manifold_models() {
1523 println!("ā ļø No pre-trained manifold models found ({e})");
1524 }
1525
1526 println!(
1527 "šÆ Engine ready: {} positions loaded from {} datasets",
1528 total_loaded,
1529 consolidated.len()
1530 );
1531 Ok(engine)
1532 }
1533
1534 pub fn new_with_instant_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1537 println!("š Initializing engine with INSTANT loading...");
1538 let mut engine = Self::new(vector_size);
1539 engine.enable_opening_book();
1540
1541 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
1543 println!("Loading complete");
1544 }
1545
1546 let discovered_files = AutoDiscovery::discover_training_files(".", false)?;
1548
1549 if discovered_files.is_empty() {
1550 println!("ā¹ļø No user training data found, loading starter dataset...");
1552 if let Err(_e) = engine.load_starter_dataset() {
1553 println!("Loading complete");
1554 println!("ā¹ļø Starting with empty engine");
1555 } else {
1556 println!(
1557 "ā
Loaded starter dataset with {} positions",
1558 engine.knowledge_base_size()
1559 );
1560 }
1561 return Ok(engine);
1562 }
1563
1564 if let Some(best_file) = discovered_files.first() {
1566 println!(
1567 "ā” Loading {} format: {}",
1568 best_file.format,
1569 best_file.path.display()
1570 );
1571 engine.load_file_by_format(&best_file.path, &best_file.format)?;
1572 println!(
1573 "ā
Loaded {} positions from {} format",
1574 engine.knowledge_base_size(),
1575 best_file.format
1576 );
1577 }
1578
1579 if let Err(e) = engine.load_manifold_models() {
1581 println!("ā ļø No pre-trained manifold models found ({e})");
1582 }
1583
1584 println!(
1585 "šÆ Engine ready: {} positions loaded",
1586 engine.knowledge_base_size()
1587 );
1588 Ok(engine)
1589 }
1590
1591 fn is_position_safe(&self, board: &Board) -> bool {
1596 match std::panic::catch_unwind(|| {
1598 use chess::MoveGen;
1599 let _legal_moves: Vec<ChessMove> = MoveGen::new_legal(board).collect();
1600 true
1601 }) {
1602 Ok(_) => true,
1603 Err(_) => {
1604 false
1606 }
1607 }
1608 }
1609
1610 pub fn check_gpu_acceleration(&self) -> Result<(), Box<dyn std::error::Error>> {
1612 match crate::gpu_acceleration::GPUAccelerator::new() {
1614 Ok(_) => {
1615 println!("š„ GPU acceleration available and ready");
1616 Ok(())
1617 }
1618 Err(_e) => Err("Processing...".to_string().into()),
1619 }
1620 }
1621
1622 pub fn load_starter_dataset(&mut self) -> Result<(), Box<dyn std::error::Error>> {
1624 let starter_data = if let Ok(file_content) =
1626 std::fs::read_to_string("training_data/starter_dataset.json")
1627 {
1628 file_content
1629 } else {
1630 r#"[
1632 {
1633 "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
1634 "evaluation": 0.0,
1635 "best_move": null,
1636 "depth": 0
1637 },
1638 {
1639 "fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
1640 "evaluation": 0.1,
1641 "best_move": "e7e5",
1642 "depth": 2
1643 },
1644 {
1645 "fen": "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
1646 "evaluation": 0.0,
1647 "best_move": "g1f3",
1648 "depth": 2
1649 }
1650 ]"#
1651 .to_string()
1652 };
1653
1654 let training_data: Vec<serde_json::Value> = serde_json::from_str(&starter_data)?;
1655
1656 for entry in training_data {
1657 if let (Some(fen), Some(evaluation)) = (entry.get("fen"), entry.get("evaluation")) {
1658 if let (Some(fen_str), Some(eval_f64)) = (fen.as_str(), evaluation.as_f64()) {
1659 match chess::Board::from_str(fen_str) {
1660 Ok(board) => {
1661 let mut eval = eval_f64 as f32;
1663
1664 if eval.abs() > 15.0 {
1667 eval /= 100.0;
1668 }
1669
1670 self.add_position(&board, eval);
1671 }
1672 Err(_) => {
1673 continue;
1675 }
1676 }
1677 }
1678 }
1679 }
1680
1681 Ok(())
1682 }
1683
1684 fn load_file_by_format(
1686 &mut self,
1687 path: &std::path::Path,
1688 format: &str,
1689 ) -> Result<(), Box<dyn std::error::Error>> {
1690 let file_size = std::fs::metadata(path)?.len();
1692
1693 if file_size > 10_000_000 {
1695 println!(
1696 "š Large file detected ({:.1} MB) - using ultra-fast loader",
1697 file_size as f64 / 1_000_000.0
1698 );
1699 return self.ultra_fast_load_any_format(path);
1700 }
1701
1702 match format {
1704 "MMAP" => self.load_training_data_mmap(path),
1705 "MSGPACK" => self.load_training_data_msgpack(path),
1706 "BINARY" => self.load_training_data_streaming_binary(path),
1707 "ZSTD" => self.load_training_data_compressed(path),
1708 "JSON" => self.load_training_data_streaming_json_v2(path),
1709 _ => Err("Processing...".to_string().into()),
1710 }
1711 }
1712
1713 pub fn ultra_fast_load_any_format<P: AsRef<std::path::Path>>(
1715 &mut self,
1716 path: P,
1717 ) -> Result<(), Box<dyn std::error::Error>> {
1718 let mut loader = UltraFastLoader::new_for_massive_datasets();
1719 loader.ultra_load_binary(path, self)?;
1720
1721 let stats = loader.get_stats();
1722 println!("š Ultra-fast loading complete:");
1723 println!(" ā
Loaded: {} positions", stats.loaded);
1724 println!("Loading complete");
1725 println!("Loading complete");
1726 println!(" š Success rate: {:.1}%", stats.success_rate() * 100.0);
1727
1728 Ok(())
1729 }
1730
1731 pub fn load_training_data_streaming_binary<P: AsRef<std::path::Path>>(
1734 &mut self,
1735 path: P,
1736 ) -> Result<(), Box<dyn std::error::Error>> {
1737 let mut loader = StreamingLoader::new();
1738 loader.stream_load_binary(path, self)?;
1739
1740 println!("š Streaming binary load complete:");
1741 println!(" Loaded: {} new positions", loader.loaded_count);
1742 println!("Loading complete");
1743 println!("Loading complete");
1744
1745 Ok(())
1746 }
1747
1748 pub fn load_training_data_streaming_json_v2<P: AsRef<std::path::Path>>(
1751 &mut self,
1752 path: P,
1753 ) -> Result<(), Box<dyn std::error::Error>> {
1754 let mut loader = StreamingLoader::new();
1755
1756 let batch_size = if std::fs::metadata(path.as_ref())?.len() > 100_000_000 {
1758 20000 } else {
1761 5000 };
1763
1764 loader.stream_load_json(path, self, batch_size)?;
1765
1766 println!("š Streaming JSON load complete:");
1767 println!(" Loaded: {} new positions", loader.loaded_count);
1768 println!("Loading complete");
1769 println!("Loading complete");
1770
1771 Ok(())
1772 }
1773
1774 pub fn new_for_massive_datasets(
1777 vector_size: usize,
1778 ) -> Result<Self, Box<dyn std::error::Error>> {
1779 println!("š Initializing engine for MASSIVE datasets (100k-1M+ positions)...");
1780 let mut engine = Self::new(vector_size);
1781 engine.enable_opening_book();
1782
1783 let discovered_files = AutoDiscovery::discover_training_files(".", false)?;
1785
1786 if discovered_files.is_empty() {
1787 println!("ā¹ļø No training data found");
1788 return Ok(engine);
1789 }
1790
1791 let largest_file = discovered_files
1793 .iter()
1794 .max_by_key(|f| f.size_bytes)
1795 .unwrap();
1796
1797 println!(
1798 "šÆ Loading largest dataset: {} ({} bytes)",
1799 largest_file.path.display(),
1800 largest_file.size_bytes
1801 );
1802
1803 engine.ultra_fast_load_any_format(&largest_file.path)?;
1805
1806 println!(
1807 "šÆ Engine ready: {} positions loaded",
1808 engine.knowledge_base_size()
1809 );
1810 Ok(engine)
1811 }
1812
1813 pub fn convert_to_msgpack() -> Result<(), Box<dyn std::error::Error>> {
1816 use serde_json::Value;
1817 use std::fs::File;
1818 use std::io::{BufReader, BufWriter};
1819
1820 if std::path::Path::new("training_data_a100.bin").exists() {
1822 Self::convert_a100_binary_to_json()?;
1823 }
1824
1825 let input_files = [
1826 "training_data.json",
1827 "tactical_training_data.json",
1828 "training_data_a100.json",
1829 ];
1830
1831 for input_file in &input_files {
1832 let input_path = std::path::Path::new(input_file);
1833 if !input_path.exists() {
1834 continue;
1835 }
1836
1837 let output_file_path = input_file.replace(".json", ".msgpack");
1838 println!("š Converting {input_file} ā {output_file_path} (MessagePack format)");
1839
1840 let file = File::open(input_path)?;
1842 let reader = BufReader::new(file);
1843 let json_value: Value = serde_json::from_reader(reader)?;
1844
1845 let data: Vec<(String, f32)> = match json_value {
1846 Value::Array(arr) if !arr.is_empty() => {
1848 if let Some(first) = arr.first() {
1849 if first.is_array() {
1850 arr.into_iter()
1852 .filter_map(|item| {
1853 if let Value::Array(tuple) = item {
1854 if tuple.len() >= 2 {
1855 let fen = tuple[0].as_str()?.to_string();
1856 let mut eval = tuple[1].as_f64()? as f32;
1857
1858 if eval.abs() > 15.0 {
1862 eval /= 100.0;
1863 }
1864
1865 Some((fen, eval))
1866 } else {
1867 None
1868 }
1869 } else {
1870 None
1871 }
1872 })
1873 .collect()
1874 } else if first.is_object() {
1875 arr.into_iter()
1877 .filter_map(|item| {
1878 if let Value::Object(obj) = item {
1879 let fen = obj.get("fen")?.as_str()?.to_string();
1880 let mut eval = obj.get("evaluation")?.as_f64()? as f32;
1881
1882 if eval.abs() > 15.0 {
1886 eval /= 100.0;
1887 }
1888
1889 Some((fen, eval))
1890 } else {
1891 None
1892 }
1893 })
1894 .collect()
1895 } else {
1896 return Err("Processing...".to_string().into());
1897 }
1898 } else {
1899 Vec::new()
1900 }
1901 }
1902 _ => return Err("Processing...".to_string().into()),
1903 };
1904
1905 if data.is_empty() {
1906 println!("Loading complete");
1907 continue;
1908 }
1909
1910 let output_file = File::create(&output_file_path)?;
1912 let mut writer = BufWriter::new(output_file);
1913 rmp_serde::encode::write(&mut writer, &data)?;
1914
1915 let input_size = input_path.metadata()?.len();
1916 let output_size = std::path::Path::new(&output_file_path).metadata()?.len();
1917 let ratio = input_size as f64 / output_size as f64;
1918
1919 println!(
1920 "ā
Converted: {} ā {} ({:.1}x size reduction, {} positions)",
1921 Self::format_bytes(input_size),
1922 Self::format_bytes(output_size),
1923 ratio,
1924 data.len()
1925 );
1926 }
1927
1928 Ok(())
1929 }
1930
1931 pub fn convert_a100_binary_to_json() -> Result<(), Box<dyn std::error::Error>> {
1933 use std::fs::File;
1934 use std::io::BufWriter;
1935
1936 let binary_path = "training_data_a100.bin";
1937 let json_path = "training_data_a100.json";
1938
1939 if !std::path::Path::new(binary_path).exists() {
1940 println!("Loading complete");
1941 return Ok(());
1942 }
1943
1944 println!("š Converting A100 binary data {binary_path} ā {json_path} (JSON format)");
1945
1946 let mut engine = ChessVectorEngine::new(1024);
1948 engine.load_training_data_binary(binary_path)?;
1949
1950 let mut data = Vec::new();
1952 for (i, board) in engine.position_boards.iter().enumerate() {
1953 if i < engine.position_evaluations.len() {
1954 data.push(serde_json::json!({
1955 "fen": board.to_string(),
1956 "evaluation": engine.position_evaluations[i],
1957 "depth": 15,
1958 "game_id": i
1959 }));
1960 }
1961 }
1962
1963 let file = File::create(json_path)?;
1965 let writer = BufWriter::new(file);
1966 serde_json::to_writer(writer, &data)?;
1967
1968 println!(
1969 "ā
Converted A100 data: {} positions ā {}",
1970 data.len(),
1971 json_path
1972 );
1973 Ok(())
1974 }
1975
1976 pub fn convert_to_zstd() -> Result<(), Box<dyn std::error::Error>> {
1979 use std::fs::File;
1980 use std::io::{BufReader, BufWriter};
1981
1982 if std::path::Path::new("training_data_a100.bin").exists() {
1984 Self::convert_a100_binary_to_json()?;
1985 }
1986
1987 let input_files = [
1988 ("training_data.json", "training_data.zst"),
1989 ("tactical_training_data.json", "tactical_training_data.zst"),
1990 ("training_data_a100.json", "training_data_a100.zst"),
1991 ("training_data.bin", "training_data.bin.zst"),
1992 (
1993 "tactical_training_data.bin",
1994 "tactical_training_data.bin.zst",
1995 ),
1996 ("training_data_a100.bin", "training_data_a100.bin.zst"),
1997 ];
1998
1999 for (input_file, output_file) in &input_files {
2000 let input_path = std::path::Path::new(input_file);
2001 if !input_path.exists() {
2002 continue;
2003 }
2004
2005 println!("š Converting {input_file} ā {output_file} (Zstd compression)");
2006
2007 let input_file = File::open(input_path)?;
2008 let output_file_handle = File::create(output_file)?;
2009 let writer = BufWriter::new(output_file_handle);
2010 let mut encoder = zstd::stream::Encoder::new(writer, 9)?; std::io::copy(&mut BufReader::new(input_file), &mut encoder)?;
2013 encoder.finish()?;
2014
2015 let input_size = input_path.metadata()?.len();
2016 let output_size = std::path::Path::new(output_file).metadata()?.len();
2017 let ratio = input_size as f64 / output_size as f64;
2018
2019 println!(
2020 "ā
Compressed: {} ā {} ({:.1}x size reduction)",
2021 Self::format_bytes(input_size),
2022 Self::format_bytes(output_size),
2023 ratio
2024 );
2025 }
2026
2027 Ok(())
2028 }
2029
2030 pub fn convert_to_mmap() -> Result<(), Box<dyn std::error::Error>> {
2033 use std::fs::File;
2034 use std::io::{BufReader, BufWriter};
2035
2036 if std::path::Path::new("training_data_a100.bin").exists() {
2038 Self::convert_a100_binary_to_json()?;
2039 }
2040
2041 let input_files = [
2042 ("training_data.json", "training_data.mmap"),
2043 ("tactical_training_data.json", "tactical_training_data.mmap"),
2044 ("training_data_a100.json", "training_data_a100.mmap"),
2045 ("training_data.msgpack", "training_data.mmap"),
2046 (
2047 "tactical_training_data.msgpack",
2048 "tactical_training_data.mmap",
2049 ),
2050 ("training_data_a100.msgpack", "training_data_a100.mmap"),
2051 ];
2052
2053 for (input_file, output_file) in &input_files {
2054 let input_path = std::path::Path::new(input_file);
2055 if !input_path.exists() {
2056 continue;
2057 }
2058
2059 println!("š Converting {input_file} ā {output_file} (Memory-mapped format)");
2060
2061 let data: Vec<(String, f32)> = if input_file.ends_with(".json") {
2063 let file = File::open(input_path)?;
2064 let reader = BufReader::new(file);
2065 let json_value: Value = serde_json::from_reader(reader)?;
2066
2067 match json_value {
2068 Value::Array(arr) if !arr.is_empty() => {
2070 if let Some(first) = arr.first() {
2071 if first.is_array() {
2072 arr.into_iter()
2074 .filter_map(|item| {
2075 if let Value::Array(tuple) = item {
2076 if tuple.len() >= 2 {
2077 let fen = tuple[0].as_str()?.to_string();
2078 let mut eval = tuple[1].as_f64()? as f32;
2079
2080 if eval.abs() > 15.0 {
2084 eval /= 100.0;
2085 }
2086
2087 Some((fen, eval))
2088 } else {
2089 None
2090 }
2091 } else {
2092 None
2093 }
2094 })
2095 .collect()
2096 } else if first.is_object() {
2097 arr.into_iter()
2099 .filter_map(|item| {
2100 if let Value::Object(obj) = item {
2101 let fen = obj.get("fen")?.as_str()?.to_string();
2102 let mut eval = obj.get("evaluation")?.as_f64()? as f32;
2103
2104 if eval.abs() > 15.0 {
2108 eval /= 100.0;
2109 }
2110
2111 Some((fen, eval))
2112 } else {
2113 None
2114 }
2115 })
2116 .collect()
2117 } else {
2118 return Err("Failed to process training data".into());
2119 }
2120 } else {
2121 Vec::new()
2122 }
2123 }
2124 _ => return Err("Processing...".to_string().into()),
2125 }
2126 } else if input_file.ends_with(".msgpack") {
2127 let file = File::open(input_path)?;
2128 let reader = BufReader::new(file);
2129 rmp_serde::from_read(reader)?
2130 } else {
2131 return Err("Unsupported input format for memory mapping".into());
2132 };
2133
2134 let output_file_handle = File::create(output_file)?;
2136 let mut writer = BufWriter::new(output_file_handle);
2137 rmp_serde::encode::write(&mut writer, &data)?;
2138
2139 let input_size = input_path.metadata()?.len();
2140 let output_size = std::path::Path::new(output_file).metadata()?.len();
2141
2142 println!(
2143 "ā
Memory-mapped file created: {} ā {} ({} positions)",
2144 Self::format_bytes(input_size),
2145 Self::format_bytes(output_size),
2146 data.len()
2147 );
2148 }
2149
2150 Ok(())
2151 }
2152
2153 pub fn convert_json_to_binary() -> Result<Vec<String>, Box<dyn std::error::Error>> {
2155 use indicatif::{ProgressBar, ProgressStyle};
2156
2157 let json_files = [
2158 "training_data.json",
2159 "tactical_training_data.json",
2160 "engine_training.json",
2161 "chess_training.json",
2162 ];
2163
2164 let existing_json_files: Vec<_> = json_files
2166 .iter()
2167 .filter(|&file_path| std::path::Path::new(file_path).exists())
2168 .collect();
2169
2170 if existing_json_files.is_empty() {
2171 println!("ā¹ļø No JSON training files found to convert");
2172 return Ok(Vec::new());
2173 }
2174
2175 println!(
2176 "š Converting {} JSON files to binary format...",
2177 existing_json_files.len()
2178 );
2179
2180 let pb = ProgressBar::new(existing_json_files.len() as u64);
2182 pb.set_style(
2183 ProgressStyle::default_bar()
2184 .template(
2185 "š¦ Converting [{elapsed_precise}] [{bar:40.yellow/blue}] {pos}/{len} {msg}",
2186 )?
2187 .progress_chars("āāā"),
2188 );
2189
2190 let mut converted_files = Vec::new();
2191
2192 for (i, json_file) in existing_json_files.iter().enumerate() {
2193 pb.set_position(i as u64);
2194 pb.set_message("Processing...".to_string());
2195
2196 let binary_file = std::path::Path::new(json_file).with_extension("bin");
2197
2198 let mut temp_engine = Self::new(1024);
2200 if temp_engine
2201 .load_training_data_incremental(json_file)
2202 .is_ok()
2203 {
2204 if temp_engine.save_training_data_binary(&binary_file).is_ok() {
2205 converted_files.push(binary_file.to_string_lossy().to_string());
2206 println!("ā
Converted {json_file} to binary format");
2207 } else {
2208 println!("Loading complete");
2209 }
2210 } else {
2211 println!("Loading complete");
2212 }
2213 }
2214
2215 pb.set_position(existing_json_files.len() as u64);
2216 pb.finish_with_message(format!("ā
Converted {} files", converted_files.len()));
2217
2218 if !converted_files.is_empty() {
2219 println!("š Binary conversion complete! Startup will be 5-15x faster next time.");
2220 println!("š Conversion summary:");
2221 for _conversion in &converted_files {
2222 println!("Loading complete");
2223 }
2224 }
2225
2226 Ok(converted_files)
2227 }
2228
2229 pub fn is_lsh_enabled(&self) -> bool {
2231 self.use_lsh
2232 }
2233
2234 pub fn lsh_stats(&self) -> Option<crate::lsh::LSHStats> {
2236 self.lsh_index.as_ref().map(|lsh| lsh.stats())
2237 }
2238
2239 pub fn enable_manifold_learning(&mut self, compression_ratio: f32) -> Result<(), String> {
2241 let input_dim = self.encoder.vector_size();
2242 let output_dim = ((input_dim as f32) / compression_ratio) as usize;
2243
2244 if output_dim == 0 {
2245 return Err("Compression ratio too high, output dimension would be 0".to_string());
2246 }
2247
2248 let mut learner = ManifoldLearner::new(input_dim, output_dim);
2249 learner.init_network()?;
2250
2251 self.manifold_learner = Some(learner);
2252 self.manifold_similarity_search = Some(SimilaritySearch::new(output_dim));
2253 self.use_manifold = false; Ok(())
2256 }
2257
2258 pub fn train_manifold_learning(&mut self, epochs: usize) -> Result<(), String> {
2260 if self.manifold_learner.is_none() {
2261 return Err(
2262 "Manifold learning not enabled. Call enable_manifold_learning first.".to_string(),
2263 );
2264 }
2265
2266 if self.similarity_search.size() == 0 {
2267 return Err("No positions in knowledge base to train on.".to_string());
2268 }
2269
2270 let rows = self.similarity_search.size();
2272 let cols = self.encoder.vector_size();
2273
2274 let training_matrix = Array2::from_shape_fn((rows, cols), |(row, col)| {
2275 if let Some((vector, _)) = self.similarity_search.get_position_ref(row) {
2276 vector[col]
2277 } else {
2278 0.0
2279 }
2280 });
2281
2282 if let Some(ref mut learner) = self.manifold_learner {
2284 learner.train(&training_matrix, epochs)?;
2285 let compression_ratio = learner.compression_ratio();
2286
2287 let _ = learner;
2289
2290 self.rebuild_manifold_indices()?;
2292 self.use_manifold = true;
2293
2294 println!(
2295 "Manifold learning training completed. Compression ratio: {compression_ratio:.1}x"
2296 );
2297 }
2298
2299 Ok(())
2300 }
2301
2302 fn rebuild_manifold_indices(&mut self) -> Result<(), String> {
2304 if let Some(ref learner) = self.manifold_learner {
2305 let output_dim = learner.output_dim();
2307 if let Some(ref mut search) = self.manifold_similarity_search {
2308 *search = SimilaritySearch::new(output_dim);
2309 }
2310 if let Some(ref mut lsh) = self.manifold_lsh_index {
2311 *lsh = LSH::new(output_dim, 8, 16); }
2313
2314 for (vector, eval) in self.similarity_search.iter_positions() {
2316 let compressed = learner.encode(vector);
2317
2318 if let Some(ref mut search) = self.manifold_similarity_search {
2319 search.add_position(compressed.clone(), eval);
2320 }
2321
2322 if let Some(ref mut lsh) = self.manifold_lsh_index {
2323 lsh.add_vector(compressed, eval);
2324 }
2325 }
2326 }
2327
2328 Ok(())
2329 }
2330
2331 pub fn enable_manifold_lsh(
2333 &mut self,
2334 num_tables: usize,
2335 hash_size: usize,
2336 ) -> Result<(), String> {
2337 if self.manifold_learner.is_none() {
2338 return Err("Manifold learning not enabled".to_string());
2339 }
2340
2341 let output_dim = self.manifold_learner.as_ref().unwrap().output_dim();
2342 self.manifold_lsh_index = Some(LSH::new(output_dim, num_tables, hash_size));
2343
2344 if self.use_manifold {
2346 self.rebuild_manifold_indices()?;
2347 }
2348
2349 Ok(())
2350 }
2351
2352 pub fn is_manifold_enabled(&self) -> bool {
2354 self.use_manifold && self.manifold_learner.is_some()
2355 }
2356
2357 pub fn manifold_compression_ratio(&self) -> Option<f32> {
2359 self.manifold_learner
2360 .as_ref()
2361 .map(|l| l.compression_ratio())
2362 }
2363
2364 pub fn load_manifold_models(&mut self) -> Result<(), Box<dyn std::error::Error>> {
2367 if let Some(ref db) = self.database {
2368 match crate::manifold_learner::ManifoldLearner::load_from_database(db)? {
2369 Some(learner) => {
2370 let compression_ratio = learner.compression_ratio();
2371 println!(
2372 "š§ Loaded pre-trained manifold learner (compression: {compression_ratio:.1}x)"
2373 );
2374
2375 self.manifold_learner = Some(learner);
2377 self.use_manifold = true;
2378
2379 self.rebuild_manifold_indices()?;
2381
2382 println!("ā
Manifold learning enabled with compressed vectors");
2383 Ok(())
2384 }
2385 None => Err("No pre-trained manifold models found in database".into()),
2386 }
2387 } else {
2388 Err("Database not initialized - cannot load manifold models".into())
2389 }
2390 }
2391
2392 pub fn enable_opening_book(&mut self) {
2394 self.opening_book = Some(OpeningBook::with_standard_openings());
2395 }
2396
2397 pub fn set_opening_book(&mut self, book: OpeningBook) {
2399 self.opening_book = Some(book);
2400 }
2401
2402 pub fn is_opening_position(&self, board: &Board) -> bool {
2404 self.opening_book
2405 .as_ref()
2406 .map(|book| book.contains(board))
2407 .unwrap_or(false)
2408 }
2409
2410 pub fn get_opening_entry(&self, board: &Board) -> Option<&OpeningEntry> {
2412 self.opening_book.as_ref()?.lookup(board)
2413 }
2414
2415 pub fn opening_book_stats(&self) -> Option<OpeningBookStats> {
2417 self.opening_book.as_ref().map(|book| book.get_statistics())
2418 }
2419
2420 pub fn add_position_with_move(
2422 &mut self,
2423 board: &Board,
2424 evaluation: f32,
2425 chess_move: Option<ChessMove>,
2426 move_outcome: Option<f32>,
2427 ) {
2428 let position_index = self.knowledge_base_size();
2429
2430 self.add_position(board, evaluation);
2432
2433 if let (Some(mov), Some(outcome)) = (chess_move, move_outcome) {
2435 self.position_moves
2436 .entry(position_index)
2437 .or_default()
2438 .push((mov, outcome));
2439 }
2440 }
2441
2442 pub fn recommend_moves(
2444 &mut self,
2445 board: &Board,
2446 num_recommendations: usize,
2447 ) -> Vec<MoveRecommendation> {
2448 if let Some(entry) = self.get_opening_entry(board) {
2462 let mut recommendations = Vec::new();
2463
2464 for (chess_move, strength) in &entry.best_moves {
2465 recommendations.push(MoveRecommendation {
2466 chess_move: *chess_move,
2467 confidence: strength * 0.9, from_similar_position_count: 1,
2469 average_outcome: entry.evaluation,
2470 });
2471 }
2472
2473 recommendations.sort_by(|a, b| {
2475 b.confidence
2476 .partial_cmp(&a.confidence)
2477 .unwrap_or(std::cmp::Ordering::Equal)
2478 });
2479 recommendations.truncate(num_recommendations);
2480 return recommendations;
2481 }
2482
2483 let similar_positions = self.find_similar_positions_with_indices(board, 20);
2485
2486 let mut move_data: HashMap<ChessMove, Vec<(f32, f32)>> = HashMap::new(); use chess::MoveGen;
2491 let legal_moves: Vec<ChessMove> = match std::panic::catch_unwind(|| {
2492 MoveGen::new_legal(board).collect::<Vec<ChessMove>>()
2493 }) {
2494 Ok(moves) => moves,
2495 Err(_) => {
2496 return Vec::new();
2498 }
2499 };
2500
2501 for (position_index, _eval, similarity) in similar_positions {
2503 if let Some(moves) = self.position_moves.get(&position_index) {
2504 for &(chess_move, outcome) in moves {
2505 if legal_moves.contains(&chess_move) {
2507 move_data
2508 .entry(chess_move)
2509 .or_default()
2510 .push((similarity, outcome));
2511 }
2512 }
2513 }
2514 }
2515
2516 if self.tactical_search.is_some() {
2518 if let Some(ref mut tactical_search) = self.tactical_search {
2519 let tactical_result = tactical_search.search(board);
2521
2522 if let Some(best_move) = tactical_result.best_move {
2524 let mut temp_board = *board;
2526 temp_board = temp_board.make_move_new(best_move);
2527 let move_evaluation = tactical_search.search(&temp_board).evaluation;
2528
2529 move_data.insert(best_move, vec![(0.95, move_evaluation)]); }
2531
2532 let mut ordered_moves = legal_moves.clone();
2535
2536 ordered_moves.sort_by(|a, b| {
2538 let a_is_capture = board.piece_on(a.get_dest()).is_some();
2539 let b_is_capture = board.piece_on(b.get_dest()).is_some();
2540
2541 match (a_is_capture, b_is_capture) {
2542 (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => {
2545 let a_centrality = move_centrality(a);
2547 let b_centrality = move_centrality(b);
2548 b_centrality
2549 .partial_cmp(&a_centrality)
2550 .unwrap_or(std::cmp::Ordering::Equal)
2551 }
2552 }
2553 });
2554
2555 for chess_move in ordered_moves.into_iter() {
2558 move_data.entry(chess_move).or_insert_with(|| {
2559 let mut temp_board = *board;
2561 temp_board = temp_board.make_move_new(chess_move);
2562 let move_evaluation = tactical_search.search(&temp_board).evaluation;
2563
2564 vec![(0.90, move_evaluation)] });
2566 }
2567 } else {
2568 let mut ordered_moves = legal_moves.clone();
2571
2572 ordered_moves.sort_by(|a, b| {
2574 let a_is_capture = board.piece_on(a.get_dest()).is_some();
2575 let b_is_capture = board.piece_on(b.get_dest()).is_some();
2576
2577 match (a_is_capture, b_is_capture) {
2578 (true, false) => std::cmp::Ordering::Less,
2579 (false, true) => std::cmp::Ordering::Greater,
2580 _ => {
2581 let a_centrality = move_centrality(a);
2582 let b_centrality = move_centrality(b);
2583 b_centrality
2584 .partial_cmp(&a_centrality)
2585 .unwrap_or(std::cmp::Ordering::Equal)
2586 }
2587 }
2588 });
2589
2590 for chess_move in ordered_moves.into_iter().take(num_recommendations) {
2591 let mut basic_eval = 0.0;
2593
2594 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
2596 basic_eval += match captured_piece {
2597 chess::Piece::Pawn => 1.0,
2598 chess::Piece::Knight | chess::Piece::Bishop => 3.0,
2599 chess::Piece::Rook => 5.0,
2600 chess::Piece::Queen => 9.0,
2601 chess::Piece::King => 100.0, };
2603 }
2604
2605 move_data.insert(chess_move, vec![(0.3, basic_eval)]); }
2607 }
2608 }
2609
2610 let mut recommendations = Vec::new();
2612
2613 for (chess_move, outcomes) in move_data {
2614 if outcomes.is_empty() {
2615 continue;
2616 }
2617
2618 let mut weighted_sum = 0.0;
2620 let mut weight_sum = 0.0;
2621
2622 for &(similarity, outcome) in &outcomes {
2623 weighted_sum += similarity * outcome;
2624 weight_sum += similarity;
2625 }
2626
2627 let average_outcome = if weight_sum > 0.0 {
2628 weighted_sum / weight_sum
2629 } else {
2630 0.0
2631 };
2632
2633 let avg_similarity =
2635 outcomes.iter().map(|(s, _)| s).sum::<f32>() / outcomes.len() as f32;
2636 let position_count_bonus = (outcomes.len() as f32).ln().max(1.0) / 5.0; let confidence = (avg_similarity * 0.8 + position_count_bonus * 0.2).min(0.95); recommendations.push(MoveRecommendation {
2640 chess_move,
2641 confidence: confidence.min(1.0), from_similar_position_count: outcomes.len(),
2643 average_outcome,
2644 });
2645 }
2646
2647 recommendations.sort_by(|a, b| {
2650 match board.side_to_move() {
2651 chess::Color::White => {
2652 b.average_outcome
2654 .partial_cmp(&a.average_outcome)
2655 .unwrap_or(std::cmp::Ordering::Equal)
2656 }
2657 chess::Color::Black => {
2658 a.average_outcome
2660 .partial_cmp(&b.average_outcome)
2661 .unwrap_or(std::cmp::Ordering::Equal)
2662 }
2663 }
2664 });
2665
2666 recommendations = self.apply_hanging_piece_safety_checks(board, recommendations);
2668
2669 recommendations.truncate(num_recommendations);
2671 recommendations
2672 }
2673
2674 fn apply_hanging_piece_safety_checks(
2677 &mut self,
2678 board: &Board,
2679 mut recommendations: Vec<MoveRecommendation>,
2680 ) -> Vec<MoveRecommendation> {
2681 use chess::{MoveGen, Piece};
2682
2683 for recommendation in &mut recommendations {
2684 let mut safety_penalty = 0.0;
2685
2686 let mut temp_board = *board;
2688 temp_board = temp_board.make_move_new(recommendation.chess_move);
2689
2690 let our_color = board.side_to_move();
2692 let opponent_color = !our_color;
2693
2694 let opponent_moves: Vec<chess::ChessMove> = MoveGen::new_legal(&temp_board).collect();
2696
2697 for square in chess::ALL_SQUARES {
2699 if let Some(piece) = temp_board.piece_on(square) {
2700 if temp_board.color_on(square) == Some(our_color) {
2701 let piece_value = match piece {
2703 Piece::Pawn => 1.0,
2704 Piece::Knight | Piece::Bishop => 3.0,
2705 Piece::Rook => 5.0,
2706 Piece::Queen => 9.0,
2707 Piece::King => 0.0, };
2709
2710 let can_be_captured =
2712 opponent_moves.iter().any(|&mv| mv.get_dest() == square);
2713
2714 if can_be_captured {
2715 let is_defended =
2717 self.is_piece_defended(&temp_board, square, our_color);
2718
2719 if !is_defended {
2720 safety_penalty += piece_value * 2.0; } else {
2723 safety_penalty += piece_value * 0.1; }
2726 }
2727 }
2728 }
2729 }
2730
2731 let original_threats = self.find_immediate_threats(board, opponent_color);
2733 let resolved_threats =
2734 self.count_resolved_threats(board, &temp_board, &original_threats);
2735
2736 if !original_threats.is_empty() && resolved_threats == 0 {
2738 safety_penalty += 2.0; }
2740
2741 let penalty_factor = 1.0 - (safety_penalty * 0.2_f32).min(0.8);
2743 recommendation.confidence *= penalty_factor;
2744 recommendation.confidence = recommendation.confidence.max(0.1);
2745
2746 recommendation.average_outcome -= safety_penalty;
2748 }
2749
2750 recommendations.sort_by(|a, b| {
2752 let confidence_cmp = b
2754 .confidence
2755 .partial_cmp(&a.confidence)
2756 .unwrap_or(std::cmp::Ordering::Equal);
2757 if confidence_cmp != std::cmp::Ordering::Equal {
2758 return confidence_cmp;
2759 }
2760
2761 match board.side_to_move() {
2763 chess::Color::White => b
2764 .average_outcome
2765 .partial_cmp(&a.average_outcome)
2766 .unwrap_or(std::cmp::Ordering::Equal),
2767 chess::Color::Black => a
2768 .average_outcome
2769 .partial_cmp(&b.average_outcome)
2770 .unwrap_or(std::cmp::Ordering::Equal),
2771 }
2772 });
2773
2774 recommendations
2775 }
2776
2777 fn is_piece_defended(
2779 &self,
2780 board: &Board,
2781 square: chess::Square,
2782 our_color: chess::Color,
2783 ) -> bool {
2784 use chess::ALL_SQUARES;
2785
2786 for source_square in ALL_SQUARES {
2788 if let Some(piece) = board.piece_on(source_square) {
2789 if board.color_on(source_square) == Some(our_color) {
2790 if self.can_piece_attack(board, piece, source_square, square) {
2792 return true;
2793 }
2794 }
2795 }
2796 }
2797
2798 false
2799 }
2800
2801 fn can_piece_attack(
2803 &self,
2804 board: &Board,
2805 piece: chess::Piece,
2806 from: chess::Square,
2807 to: chess::Square,
2808 ) -> bool {
2809 use chess::Piece;
2810
2811 match piece {
2814 Piece::Pawn => {
2815 let from_file = from.get_file().to_index();
2817 let from_rank = from.get_rank().to_index();
2818 let to_file = to.get_file().to_index();
2819 let to_rank = to.get_rank().to_index();
2820
2821 let file_diff = (to_file as i32 - from_file as i32).abs();
2822 let rank_diff = to_rank as i32 - from_rank as i32;
2823
2824 file_diff == 1 && {
2826 match board.color_on(from).unwrap() {
2827 chess::Color::White => rank_diff == 1,
2828 chess::Color::Black => rank_diff == -1,
2829 }
2830 }
2831 }
2832 Piece::Knight => {
2833 let from_file = from.get_file().to_index() as i32;
2835 let from_rank = from.get_rank().to_index() as i32;
2836 let to_file = to.get_file().to_index() as i32;
2837 let to_rank = to.get_rank().to_index() as i32;
2838
2839 let file_diff = (to_file - from_file).abs();
2840 let rank_diff = (to_rank - from_rank).abs();
2841
2842 (file_diff == 2 && rank_diff == 1) || (file_diff == 1 && rank_diff == 2)
2843 }
2844 Piece::Bishop => {
2845 self.is_diagonal_clear(board, from, to)
2847 }
2848 Piece::Rook => {
2849 self.is_straight_clear(board, from, to)
2851 }
2852 Piece::Queen => {
2853 self.is_diagonal_clear(board, from, to) || self.is_straight_clear(board, from, to)
2855 }
2856 Piece::King => {
2857 let from_file = from.get_file().to_index() as i32;
2859 let from_rank = from.get_rank().to_index() as i32;
2860 let to_file = to.get_file().to_index() as i32;
2861 let to_rank = to.get_rank().to_index() as i32;
2862
2863 let file_diff = (to_file - from_file).abs();
2864 let rank_diff = (to_rank - from_rank).abs();
2865
2866 file_diff <= 1 && rank_diff <= 1 && (file_diff != 0 || rank_diff != 0)
2867 }
2868 }
2869 }
2870
2871 fn is_diagonal_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
2873 let from_file = from.get_file().to_index() as i32;
2874 let from_rank = from.get_rank().to_index() as i32;
2875 let to_file = to.get_file().to_index() as i32;
2876 let to_rank = to.get_rank().to_index() as i32;
2877
2878 let file_diff = to_file - from_file;
2879 let rank_diff = to_rank - from_rank;
2880
2881 if file_diff.abs() != rank_diff.abs() || file_diff == 0 {
2883 return false;
2884 }
2885
2886 let file_step = if file_diff > 0 { 1 } else { -1 };
2887 let rank_step = if rank_diff > 0 { 1 } else { -1 };
2888
2889 let steps = file_diff.abs();
2890
2891 for i in 1..steps {
2893 let check_file = from_file + i * file_step;
2894 let check_rank = from_rank + i * rank_step;
2895
2896 let check_square = chess::Square::make_square(
2897 chess::Rank::from_index(check_rank as usize),
2898 chess::File::from_index(check_file as usize),
2899 );
2900 if board.piece_on(check_square).is_some() {
2901 return false; }
2903 }
2904
2905 true
2906 }
2907
2908 fn is_straight_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
2910 let from_file = from.get_file().to_index() as i32;
2911 let from_rank = from.get_rank().to_index() as i32;
2912 let to_file = to.get_file().to_index() as i32;
2913 let to_rank = to.get_rank().to_index() as i32;
2914
2915 if from_file != to_file && from_rank != to_rank {
2917 return false;
2918 }
2919
2920 if from_file == to_file {
2921 let start_rank = from_rank.min(to_rank);
2923 let end_rank = from_rank.max(to_rank);
2924
2925 for rank in (start_rank + 1)..end_rank {
2926 let check_square = chess::Square::make_square(
2927 chess::Rank::from_index(rank as usize),
2928 chess::File::from_index(from_file as usize),
2929 );
2930 if board.piece_on(check_square).is_some() {
2931 return false; }
2933 }
2934 } else {
2935 let start_file = from_file.min(to_file);
2937 let end_file = from_file.max(to_file);
2938
2939 for file in (start_file + 1)..end_file {
2940 let check_square = chess::Square::make_square(
2941 chess::Rank::from_index(from_rank as usize),
2942 chess::File::from_index(file as usize),
2943 );
2944 if board.piece_on(check_square).is_some() {
2945 return false; }
2947 }
2948 }
2949
2950 true
2951 }
2952
2953 fn find_immediate_threats(
2955 &self,
2956 board: &Board,
2957 opponent_color: chess::Color,
2958 ) -> Vec<(chess::Square, f32)> {
2959 use chess::MoveGen;
2960
2961 let mut threats = Vec::new();
2962
2963 let opponent_moves: Vec<chess::ChessMove> = MoveGen::new_legal(board).collect();
2965
2966 for mv in opponent_moves {
2967 let target_square = mv.get_dest();
2968 if let Some(piece) = board.piece_on(target_square) {
2969 if board.color_on(target_square) == Some(!opponent_color) {
2970 let piece_value = match piece {
2972 chess::Piece::Pawn => 1.0,
2973 chess::Piece::Knight | chess::Piece::Bishop => 3.0,
2974 chess::Piece::Rook => 5.0,
2975 chess::Piece::Queen => 9.0,
2976 chess::Piece::King => 100.0,
2977 };
2978 threats.push((target_square, piece_value));
2979 }
2980 }
2981 }
2982
2983 threats
2984 }
2985
2986 fn count_resolved_threats(
2988 &self,
2989 original_board: &Board,
2990 new_board: &Board,
2991 original_threats: &[(chess::Square, f32)],
2992 ) -> usize {
2993 let mut resolved = 0;
2994
2995 for &(threatened_square, _value) in original_threats {
2996 let piece_still_there =
2998 new_board.piece_on(threatened_square) == original_board.piece_on(threatened_square);
2999
3000 if !piece_still_there {
3001 resolved += 1;
3003 } else {
3004 let still_threatened = self
3006 .find_immediate_threats(new_board, new_board.side_to_move())
3007 .iter()
3008 .any(|&(square, _)| square == threatened_square);
3009
3010 if !still_threatened {
3011 resolved += 1;
3012 }
3013 }
3014 }
3015
3016 resolved
3017 }
3018
3019 pub fn recommend_legal_moves(
3021 &mut self,
3022 board: &Board,
3023 num_recommendations: usize,
3024 ) -> Vec<MoveRecommendation> {
3025 use chess::MoveGen;
3026
3027 let legal_moves: std::collections::HashSet<ChessMove> = MoveGen::new_legal(board).collect();
3029
3030 let all_recommendations = self.recommend_moves(board, num_recommendations * 2); all_recommendations
3034 .into_iter()
3035 .filter(|rec| legal_moves.contains(&rec.chess_move))
3036 .take(num_recommendations)
3037 .collect()
3038 }
3039
3040 pub fn enable_persistence<P: AsRef<Path>>(
3042 &mut self,
3043 db_path: P,
3044 ) -> Result<(), Box<dyn std::error::Error>> {
3045 let database = Database::new(db_path)?;
3046 self.database = Some(database);
3047 println!("Persistence enabled");
3048 Ok(())
3049 }
3050
3051 pub fn save_to_database(&self) -> Result<(), Box<dyn std::error::Error>> {
3053 let db = self
3054 .database
3055 .as_ref()
3056 .ok_or("Database not enabled. Call enable_persistence() first.")?;
3057
3058 println!("š¾ Saving engine state to database (batch mode)...");
3059
3060 let current_time = std::time::SystemTime::now()
3062 .duration_since(std::time::UNIX_EPOCH)?
3063 .as_secs() as i64;
3064
3065 let mut position_data_batch = Vec::with_capacity(self.position_boards.len());
3066
3067 for (i, board) in self.position_boards.iter().enumerate() {
3068 if i < self.position_vectors.len() && i < self.position_evaluations.len() {
3069 let vector = self.position_vectors[i].as_slice().unwrap();
3070 let position_data = PositionData {
3071 fen: board.to_string(),
3072 vector: vector.iter().map(|&x| x as f64).collect(),
3073 evaluation: Some(self.position_evaluations[i] as f64),
3074 compressed_vector: None, created_at: current_time,
3076 };
3077 position_data_batch.push(position_data);
3078 }
3079 }
3080
3081 if !position_data_batch.is_empty() {
3083 let saved_count = db.save_positions_batch(&position_data_batch)?;
3084 println!("š Batch saved {saved_count} positions");
3085 }
3086
3087 if let Some(ref lsh) = self.lsh_index {
3089 lsh.save_to_database(db)?;
3090 }
3091
3092 if let Some(ref learner) = self.manifold_learner {
3094 if learner.is_trained() {
3095 learner.save_to_database(db)?;
3096 }
3097 }
3098
3099 println!("ā
Engine state saved successfully (batch optimized)");
3100 Ok(())
3101 }
3102
3103 pub fn load_from_database(&mut self) -> Result<(), Box<dyn std::error::Error>> {
3105 let db = self
3106 .database
3107 .as_ref()
3108 .ok_or("Database not enabled. Call enable_persistence() first.")?;
3109
3110 println!("Loading engine state from database...");
3111
3112 let positions = db.load_all_positions()?;
3114 for position_data in positions {
3115 if let Ok(board) = Board::from_str(&position_data.fen) {
3116 let vector: Vec<f32> = position_data.vector.iter().map(|&x| x as f32).collect();
3117 let vector_array = Array1::from(vector);
3118 let mut evaluation = position_data.evaluation.unwrap_or(0.0) as f32;
3119
3120 if evaluation.abs() > 15.0 {
3124 evaluation /= 100.0;
3125 }
3126
3127 self.similarity_search
3129 .add_position(vector_array.clone(), evaluation);
3130
3131 self.position_vectors.push(vector_array);
3133 self.position_boards.push(board);
3134 self.position_evaluations.push(evaluation);
3135 }
3136 }
3137
3138 if self.use_lsh {
3140 let positions_for_lsh: Vec<(Array1<f32>, f32)> = self
3141 .position_vectors
3142 .iter()
3143 .zip(self.position_evaluations.iter())
3144 .map(|(v, &e)| (v.clone(), e))
3145 .collect();
3146
3147 match LSH::load_from_database(db, &positions_for_lsh)? {
3148 Some(lsh) => {
3149 self.lsh_index = Some(lsh);
3150 println!("Loaded LSH configuration from database");
3151 }
3152 None => {
3153 println!("No LSH configuration found in database");
3154 }
3155 }
3156 }
3157
3158 match ManifoldLearner::load_from_database(db)? {
3160 Some(learner) => {
3161 self.manifold_learner = Some(learner);
3162 if self.use_manifold {
3163 self.rebuild_manifold_indices()?;
3164 }
3165 println!("Loaded manifold learner from database");
3166 }
3167 None => {
3168 println!("No manifold learner found in database");
3169 }
3170 }
3171
3172 println!(
3173 "Engine state loaded successfully ({} positions)",
3174 self.knowledge_base_size()
3175 );
3176 Ok(())
3177 }
3178
3179 pub fn new_with_persistence<P: AsRef<Path>>(
3181 vector_size: usize,
3182 db_path: P,
3183 ) -> Result<Self, Box<dyn std::error::Error>> {
3184 let mut engine = Self::new(vector_size);
3185 engine.enable_persistence(db_path)?;
3186
3187 match engine.load_from_database() {
3189 Ok(_) => {
3190 println!("Loaded existing engine from database");
3191 }
3192 Err(e) => {
3193 println!("Starting fresh engine (load failed: {e})");
3194 }
3195 }
3196
3197 Ok(engine)
3198 }
3199
3200 pub fn auto_save(&self) -> Result<(), Box<dyn std::error::Error>> {
3202 if self.database.is_some() {
3203 self.save_to_database()?;
3204 }
3205 Ok(())
3206 }
3207
3208 pub fn is_persistence_enabled(&self) -> bool {
3210 self.database.is_some()
3211 }
3212
3213 pub fn database_position_count(&self) -> Result<i64, Box<dyn std::error::Error>> {
3215 let db = self.database.as_ref().ok_or("Database not enabled")?;
3216 Ok(db.get_position_count()?)
3217 }
3218
3219 pub fn enable_tactical_search(&mut self, config: TacticalConfig) {
3221 self.tactical_search = Some(TacticalSearch::new(config));
3222 }
3223
3224 pub fn enable_tactical_search_default(&mut self) {
3226 self.tactical_search = Some(TacticalSearch::new_default());
3227 }
3228
3229 pub fn configure_hybrid_evaluation(&mut self, config: HybridConfig) {
3231 self.hybrid_config = config;
3232 }
3233
3234 pub fn is_tactical_search_enabled(&self) -> bool {
3236 self.tactical_search.is_some()
3237 }
3238
3239 pub fn enable_parallel_search(&mut self, num_threads: usize) {
3241 if let Some(ref mut tactical_search) = self.tactical_search {
3242 tactical_search.config.enable_parallel_search = true;
3243 tactical_search.config.num_threads = num_threads;
3244 println!("š§µ Parallel tactical search enabled with {num_threads} threads");
3245 }
3246 }
3247
3248 pub fn is_parallel_search_enabled(&self) -> bool {
3250 self.tactical_search
3251 .as_ref()
3252 .map(|ts| ts.config.enable_parallel_search)
3253 .unwrap_or(false)
3254 }
3255
3256 pub fn enable_nnue(&mut self) -> Result<(), Box<dyn std::error::Error>> {
3278 self.enable_nnue_with_auto_load(true)
3279 }
3280
3281 pub fn enable_nnue_with_auto_load(
3283 &mut self,
3284 auto_load: bool,
3285 ) -> Result<(), Box<dyn std::error::Error>> {
3286 let config = NNUEConfig::default();
3287 let mut nnue = NNUE::new(config)?;
3288
3289 if auto_load {
3291 if let Err(e) = self.try_load_default_nnue_model(&mut nnue) {
3292 println!("š Default NNUE model not found, using fresh model: {}", e);
3293 println!(
3294 " š” Create one with: cargo run --bin train_nnue -- --output default_hybrid"
3295 );
3296 } else {
3297 println!("ā
Auto-loaded default NNUE model (default_hybrid.config)");
3298
3299 if !nnue.are_weights_loaded() {
3301 println!("ā ļø Weights not properly applied, will use quick training fallback");
3302 } else {
3303 println!("ā
Weights successfully applied to feature transformer");
3304 }
3305 }
3306 }
3307
3308 self.nnue = Some(nnue);
3309 Ok(())
3310 }
3311
3312 fn try_load_default_nnue_model(
3314 &self,
3315 nnue: &mut NNUE,
3316 ) -> Result<(), Box<dyn std::error::Error>> {
3317 let default_paths = [
3319 "default_hybrid", "production_hybrid", "hybrid_production_nnue", "chess_nnue_advanced", "trained_nnue_model", ];
3325
3326 for path in &default_paths {
3327 let config_path = format!("{}.config", path);
3328 if std::path::Path::new(&config_path).exists() {
3329 nnue.load_model(path)?;
3330 return Ok(());
3331 }
3332 }
3333
3334 Err("No default NNUE model found in standard locations".into())
3335 }
3336
3337 pub fn enable_nnue_with_config(
3339 &mut self,
3340 config: NNUEConfig,
3341 ) -> Result<(), Box<dyn std::error::Error>> {
3342 self.nnue = Some(NNUE::new(config)?);
3343 Ok(())
3344 }
3345
3346 pub fn enable_nnue_with_model(
3348 &mut self,
3349 model_path: &str,
3350 ) -> Result<(), Box<dyn std::error::Error>> {
3351 let config = NNUEConfig::default();
3352 let mut nnue = NNUE::new(config)?;
3353 nnue.load_model(model_path)?;
3354 self.nnue = Some(nnue);
3355 Ok(())
3356 }
3357
3358 pub fn quick_fix_nnue_if_needed(&mut self) -> Result<(), Box<dyn std::error::Error>> {
3360 if let Some(ref mut nnue) = self.nnue {
3361 if !nnue.are_weights_loaded() {
3362 let training_positions = vec![(chess::Board::default(), 0.0)];
3364
3365 let mut positions = training_positions;
3367 if let Ok(board) = chess::Board::from_str(
3368 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
3369 ) {
3370 positions.push((board, 0.25));
3371 }
3372 if let Ok(board) = chess::Board::from_str("8/8/8/8/8/8/1K6/k6Q w - - 0 1") {
3373 positions.push((board, 9.0));
3374 }
3375
3376 nnue.quick_fix_training(&positions)?;
3377 }
3378 }
3379 Ok(())
3380 }
3381
3382 pub fn configure_nnue(&mut self, config: NNUEConfig) -> Result<(), Box<dyn std::error::Error>> {
3384 if self.nnue.is_some() {
3385 self.nnue = Some(NNUE::new(config)?);
3386 Ok(())
3387 } else {
3388 Err("NNUE must be enabled first before configuring".into())
3389 }
3390 }
3391
3392 pub fn is_nnue_enabled(&self) -> bool {
3394 self.nnue.is_some()
3395 }
3396
3397 pub fn train_nnue(
3399 &mut self,
3400 positions: &[(Board, f32)],
3401 ) -> Result<f32, Box<dyn std::error::Error>> {
3402 if let Some(ref mut nnue) = self.nnue {
3403 let loss = nnue.train_batch(positions)?;
3404 Ok(loss)
3405 } else {
3406 Err("NNUE must be enabled before training".into())
3407 }
3408 }
3409
3410 pub fn hybrid_config(&self) -> &HybridConfig {
3412 &self.hybrid_config
3413 }
3414
3415 pub fn is_opening_book_enabled(&self) -> bool {
3417 self.opening_book.is_some()
3418 }
3419
3420 pub fn self_play_training(
3422 &mut self,
3423 config: training::SelfPlayConfig,
3424 ) -> Result<usize, Box<dyn std::error::Error>> {
3425 let mut trainer = training::SelfPlayTrainer::new(config);
3426 let new_data = trainer.generate_training_data(self);
3427
3428 let positions_added = new_data.data.len();
3429
3430 for data in &new_data.data {
3432 self.add_position(&data.board, data.evaluation);
3433 }
3434
3435 if self.database.is_some() {
3437 match self.save_to_database() {
3438 Ok(_) => println!("š¾ Saved {positions_added} positions to database"),
3439 Err(_e) => println!("Loading complete"),
3440 }
3441 }
3442
3443 println!("š§ Self-play training complete: {positions_added} new positions learned");
3444 Ok(positions_added)
3445 }
3446
3447 pub fn continuous_self_play(
3449 &mut self,
3450 config: training::SelfPlayConfig,
3451 iterations: usize,
3452 save_path: Option<&str>,
3453 ) -> Result<usize, Box<dyn std::error::Error>> {
3454 let mut total_positions = 0;
3455 let mut trainer = training::SelfPlayTrainer::new(config.clone());
3456
3457 println!("š Starting continuous self-play training for {iterations} iterations...");
3458
3459 for iteration in 1..=iterations {
3460 println!("\n--- Self-Play Iteration {iteration}/{iterations} ---");
3461
3462 let new_data = trainer.generate_training_data(self);
3464 let batch_size = new_data.data.len();
3465
3466 for data in &new_data.data {
3468 self.add_position(&data.board, data.evaluation);
3469 }
3470
3471 total_positions += batch_size;
3472
3473 println!(
3474 "ā
Iteration {}: Added {} positions (total: {})",
3475 iteration,
3476 batch_size,
3477 self.knowledge_base_size()
3478 );
3479
3480 if iteration % 5 == 0 || iteration == iterations {
3482 if let Some(path) = save_path {
3484 match self.save_training_data_binary(path) {
3485 Ok(_) => println!("š¾ Progress saved to {path} (binary format)"),
3486 Err(_e) => println!("Loading complete"),
3487 }
3488 }
3489
3490 if self.database.is_some() {
3492 match self.save_to_database() {
3493 Ok(_) => println!(
3494 "š¾ Database synchronized ({} total positions)",
3495 self.knowledge_base_size()
3496 ),
3497 Err(_e) => println!("Loading complete"),
3498 }
3499 }
3500 }
3501
3502 if iteration % 10 == 0
3504 && self.knowledge_base_size() > 5000
3505 && self.manifold_learner.is_some()
3506 {
3507 println!("š§ Retraining manifold learning with new data...");
3508 let _ = self.train_manifold_learning(5);
3509 }
3510 }
3511
3512 println!("\nš Continuous self-play complete: {total_positions} total new positions");
3513 Ok(total_positions)
3514 }
3515
3516 pub fn adaptive_self_play(
3518 &mut self,
3519 base_config: training::SelfPlayConfig,
3520 target_strength: f32,
3521 ) -> Result<usize, Box<dyn std::error::Error>> {
3522 let mut current_config = base_config;
3523 let mut total_positions = 0;
3524 let mut iteration = 1;
3525
3526 println!(
3527 "šÆ Starting adaptive self-play training (target strength: {target_strength:.2})..."
3528 );
3529
3530 loop {
3531 println!("\n--- Adaptive Iteration {iteration} ---");
3532
3533 let positions_added = self.self_play_training(current_config.clone())?;
3535 total_positions += positions_added;
3536
3537 if self.database.is_some() {
3539 match self.save_to_database() {
3540 Ok(_) => println!("š¾ Adaptive training progress saved to database"),
3541 Err(_e) => println!("Loading complete"),
3542 }
3543 }
3544
3545 let current_strength = self.knowledge_base_size() as f32 / 10000.0; println!(
3549 "š Current strength estimate: {current_strength:.2} (target: {target_strength:.2})"
3550 );
3551
3552 if current_strength >= target_strength {
3553 println!("š Target strength reached!");
3554 break;
3555 }
3556
3557 current_config.exploration_factor *= 0.95; current_config.temperature *= 0.98; current_config.games_per_iteration =
3561 (current_config.games_per_iteration as f32 * 1.1) as usize; iteration += 1;
3564
3565 if iteration > 50 {
3566 println!("ā ļø Maximum iterations reached");
3567 break;
3568 }
3569 }
3570
3571 Ok(total_positions)
3572 }
3573}
3574
3575#[cfg(test)]
3576mod tests {
3577 use super::*;
3578 use chess::Board;
3579
3580 #[test]
3581 fn test_engine_creation() {
3582 let engine = ChessVectorEngine::new(1024);
3583 assert_eq!(engine.knowledge_base_size(), 0);
3584 }
3585
3586 #[test]
3587 fn test_add_and_search() {
3588 let mut engine = ChessVectorEngine::new(1024);
3589 let board = Board::default();
3590
3591 engine.add_position(&board, 0.0);
3592 assert_eq!(engine.knowledge_base_size(), 1);
3593
3594 let similar = engine.find_similar_positions(&board, 1);
3595 assert_eq!(similar.len(), 1);
3596 }
3597
3598 #[test]
3599 fn test_evaluation() {
3600 let mut engine = ChessVectorEngine::new(1024);
3601 let board = Board::default();
3602
3603 engine.add_position(&board, 0.5);
3605
3606 let evaluation = engine.evaluate_position(&board);
3607 assert!(evaluation.is_some());
3608 assert!((evaluation.unwrap() - 0.5).abs() < 1e-6);
3609 }
3610
3611 #[test]
3612 fn test_move_recommendations() {
3613 let mut engine = ChessVectorEngine::new(1024);
3614 let board = Board::default();
3615
3616 use chess::ChessMove;
3618 use std::str::FromStr;
3619 let mov = ChessMove::from_str("e2e4").unwrap();
3620 engine.add_position_with_move(&board, 0.0, Some(mov), Some(0.8));
3621
3622 let recommendations = engine.recommend_moves(&board, 3);
3623 assert!(!recommendations.is_empty());
3624
3625 let legal_recommendations = engine.recommend_legal_moves(&board, 3);
3627 assert!(!legal_recommendations.is_empty());
3628 }
3629
3630 #[test]
3631 fn test_empty_knowledge_base_fallback() {
3632 let mut engine = ChessVectorEngine::new(1024);
3634
3635 use std::str::FromStr;
3637 let board =
3638 Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 0 1")
3639 .unwrap();
3640
3641 let recommendations = engine.recommend_moves(&board, 5);
3643 assert!(
3644 !recommendations.is_empty(),
3645 "recommend_moves should not return empty even with no training data"
3646 );
3647 assert_eq!(
3648 recommendations.len(),
3649 5,
3650 "Should return exactly 5 recommendations"
3651 );
3652
3653 for rec in &recommendations {
3655 assert!(rec.confidence > 0.0, "Confidence should be greater than 0");
3656 assert_eq!(
3657 rec.from_similar_position_count, 1,
3658 "Should have count of 1 for fallback"
3659 );
3660 assert_eq!(rec.average_outcome, 0.0, "Should have neutral outcome");
3661 }
3662
3663 let starting_board = Board::default();
3665 let starting_recommendations = engine.recommend_moves(&starting_board, 3);
3666 assert!(
3667 !starting_recommendations.is_empty(),
3668 "Should work for starting position too"
3669 );
3670
3671 use chess::MoveGen;
3673 let legal_moves: std::collections::HashSet<_> = MoveGen::new_legal(&board).collect();
3674 for rec in &recommendations {
3675 assert!(
3676 legal_moves.contains(&rec.chess_move),
3677 "All recommended moves should be legal"
3678 );
3679 }
3680 }
3681
3682 #[test]
3683 fn test_opening_book_integration() {
3684 let mut engine = ChessVectorEngine::new(1024);
3685
3686 engine.enable_opening_book();
3688 assert!(engine.opening_book.is_some());
3689
3690 let board = Board::default();
3692 assert!(engine.is_opening_position(&board));
3693
3694 let entry = engine.get_opening_entry(&board);
3695 assert!(entry.is_some());
3696
3697 let stats = engine.opening_book_stats();
3698 assert!(stats.is_some());
3699 assert!(stats.unwrap().total_openings > 0);
3700
3701 let recommendations = engine.recommend_moves(&board, 3);
3703 assert!(!recommendations.is_empty());
3704 assert!(recommendations[0].confidence > 0.7); }
3706
3707 #[test]
3708 fn test_manifold_learning_integration() {
3709 let mut engine = ChessVectorEngine::new(1024);
3710
3711 let board = Board::default();
3713 for i in 0..10 {
3714 engine.add_position(&board, i as f32 * 0.1);
3715 }
3716
3717 assert!(engine.enable_manifold_learning(8.0).is_ok());
3719
3720 let ratio = engine.manifold_compression_ratio();
3722 assert!(ratio.is_some());
3723 assert!((ratio.unwrap() - 8.0).abs() < 0.1);
3724
3725 assert!(engine.train_manifold_learning(5).is_ok());
3727
3728 let original_similar = engine.find_similar_positions(&board, 3);
3730 assert!(!original_similar.is_empty());
3731 }
3732
3733 #[test]
3734 fn test_lsh_integration() {
3735 let mut engine = ChessVectorEngine::new(1024);
3736
3737 let board = Board::default();
3739 for i in 0..50 {
3740 engine.add_position(&board, i as f32 * 0.02);
3741 }
3742
3743 engine.enable_lsh(4, 8);
3745
3746 let similar = engine.find_similar_positions(&board, 5);
3748 assert!(!similar.is_empty());
3749 assert!(similar.len() <= 5);
3750
3751 let eval = engine.evaluate_position(&board);
3753 assert!(eval.is_some());
3754 }
3755
3756 #[test]
3757 fn test_manifold_lsh_integration() {
3758 let mut engine = ChessVectorEngine::new(1024);
3759
3760 let board = Board::default();
3762 for i in 0..20 {
3763 engine.add_position(&board, i as f32 * 0.05);
3764 }
3765
3766 assert!(engine.enable_manifold_learning(8.0).is_ok());
3768 assert!(engine.train_manifold_learning(3).is_ok());
3769
3770 assert!(engine.enable_manifold_lsh(4, 8).is_ok());
3772
3773 let similar = engine.find_similar_positions(&board, 3);
3775 assert!(!similar.is_empty());
3776
3777 let _recommendations = engine.recommend_moves(&board, 2);
3779 }
3781
3782 #[test]
3807 fn test_position_with_move_storage() {
3808 let mut engine = ChessVectorEngine::new(1024);
3809 let board = Board::default();
3810
3811 use chess::ChessMove;
3812 use std::str::FromStr;
3813 let move1 = ChessMove::from_str("e2e4").unwrap();
3814 let move2 = ChessMove::from_str("d2d4").unwrap();
3815
3816 engine.add_position_with_move(&board, 0.0, Some(move1), Some(0.7));
3818 engine.add_position_with_move(&board, 0.1, Some(move2), Some(0.6));
3819
3820 assert_eq!(engine.position_moves.len(), 2);
3822
3823 let recommendations = engine.recommend_moves(&board, 5);
3825 let _move_strings: Vec<String> = recommendations
3826 .iter()
3827 .map(|r| r.chess_move.to_string())
3828 .collect();
3829
3830 assert!(!recommendations.is_empty());
3832 }
3833
3834 #[test]
3835 fn test_performance_regression_basic() {
3836 use std::time::Instant;
3837
3838 let mut engine = ChessVectorEngine::new(1024);
3839 let board = Board::default();
3840
3841 for i in 0..100 {
3843 engine.add_position(&board, i as f32 * 0.01);
3844 }
3845
3846 let start = Instant::now();
3848
3849 for _ in 0..100 {
3851 engine.add_position(&board, 0.0);
3852 }
3853
3854 let encoding_time = start.elapsed();
3855
3856 let start = Instant::now();
3858 for _ in 0..10 {
3859 engine.find_similar_positions(&board, 5);
3860 }
3861 let search_time = start.elapsed();
3862
3863 assert!(
3865 encoding_time.as_millis() < 10000,
3866 "Position encoding too slow: {}ms",
3867 encoding_time.as_millis()
3868 );
3869 assert!(
3870 search_time.as_millis() < 5000,
3871 "Search too slow: {}ms",
3872 search_time.as_millis()
3873 );
3874 }
3875
3876 #[test]
3877 fn test_memory_usage_reasonable() {
3878 let mut engine = ChessVectorEngine::new(1024);
3879 let board = Board::default();
3880
3881 let initial_size = engine.knowledge_base_size();
3883
3884 for i in 0..1000 {
3885 engine.add_position(&board, i as f32 * 0.001);
3886 }
3887
3888 let final_size = engine.knowledge_base_size();
3889 assert_eq!(final_size, initial_size + 1000);
3890
3891 assert!(final_size > initial_size);
3893 }
3894
3895 #[test]
3896 fn test_incremental_training() {
3897 use std::str::FromStr;
3898
3899 let mut engine = ChessVectorEngine::new(1024);
3900 let board1 = Board::default();
3901 let board2 =
3902 Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap();
3903
3904 engine.add_position(&board1, 0.0);
3906 engine.add_position(&board2, 0.2);
3907 assert_eq!(engine.knowledge_base_size(), 2);
3908
3909 let mut dataset = crate::training::TrainingDataset::new();
3911 dataset.add_position(board1, 0.1, 15, 1); dataset.add_position(
3913 Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
3914 .unwrap(),
3915 0.3,
3916 15,
3917 2,
3918 ); engine.train_from_dataset_incremental(&dataset);
3922
3923 assert_eq!(engine.knowledge_base_size(), 3);
3925
3926 let stats = engine.training_stats();
3928 assert_eq!(stats.total_positions, 3);
3929 assert_eq!(stats.unique_positions, 3);
3930 assert!(!stats.has_move_data); }
3932
3933 #[test]
3934 fn test_save_load_incremental() {
3935 use std::str::FromStr;
3936 use tempfile::tempdir;
3937
3938 let temp_dir = tempdir().unwrap();
3939 let file_path = temp_dir.path().join("test_training.json");
3940
3941 let mut engine1 = ChessVectorEngine::new(1024);
3943 engine1.add_position(&Board::default(), 0.0);
3944 engine1.add_position(
3945 &Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap(),
3946 0.2,
3947 );
3948
3949 engine1.save_training_data(&file_path).unwrap();
3951
3952 let mut engine2 = ChessVectorEngine::new(1024);
3954 engine2.add_position(
3955 &Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
3956 .unwrap(),
3957 0.3,
3958 );
3959 assert_eq!(engine2.knowledge_base_size(), 1);
3960
3961 engine2.load_training_data_incremental(&file_path).unwrap();
3963
3964 assert_eq!(engine2.knowledge_base_size(), 3);
3966 }
3967
3968 #[test]
3969 fn test_training_stats() {
3970 use std::str::FromStr;
3971
3972 let mut engine = ChessVectorEngine::new(1024);
3973
3974 let stats = engine.training_stats();
3976 assert_eq!(stats.total_positions, 0);
3977 assert_eq!(stats.unique_positions, 0);
3978 assert!(!stats.has_move_data);
3979 assert!(!stats.lsh_enabled);
3980 assert!(!stats.manifold_enabled);
3981 assert!(!stats.opening_book_enabled);
3982
3983 engine.add_position(&Board::default(), 0.0);
3985 engine.add_position_with_move(
3986 &Board::default(),
3987 0.1,
3988 Some(ChessMove::from_str("e2e4").unwrap()),
3989 Some(0.8),
3990 );
3991
3992 engine.enable_opening_book();
3994 engine.enable_lsh(4, 8);
3995
3996 let stats = engine.training_stats();
3997 assert_eq!(stats.total_positions, 2);
3998 assert!(stats.has_move_data);
3999 assert!(stats.move_data_entries > 0);
4000 assert!(stats.lsh_enabled);
4001 assert!(stats.opening_book_enabled);
4002 }
4003
4004 #[test]
4005 fn test_tactical_search_integration() {
4006 let mut engine = ChessVectorEngine::new(1024);
4007 let board = Board::default();
4008
4009 assert!(!engine.is_tactical_search_enabled());
4011
4012 engine.enable_tactical_search_default();
4014 assert!(engine.is_tactical_search_enabled());
4015
4016 let evaluation = engine.evaluate_position(&board);
4018 assert!(evaluation.is_some());
4019
4020 engine.add_position(&board, 0.5);
4022 let hybrid_evaluation = engine.evaluate_position(&board);
4023 assert!(hybrid_evaluation.is_some());
4024 }
4025
4026 #[test]
4027 fn test_hybrid_evaluation_configuration() {
4028 let mut engine = ChessVectorEngine::new(1024);
4029 let board = Board::default();
4030
4031 engine.enable_tactical_search_default();
4033
4034 let custom_config = HybridConfig {
4036 pattern_confidence_threshold: 0.9, enable_tactical_refinement: true,
4038 tactical_config: TacticalConfig::default(),
4039 pattern_weight: 0.8,
4040 min_similar_positions: 5,
4041 };
4042
4043 engine.configure_hybrid_evaluation(custom_config);
4044
4045 engine.add_position(&board, 0.3);
4047
4048 let evaluation = engine.evaluate_position(&board);
4049 assert!(evaluation.is_some());
4050
4051 let no_tactical_config = HybridConfig {
4053 enable_tactical_refinement: false,
4054 ..HybridConfig::default()
4055 };
4056
4057 engine.configure_hybrid_evaluation(no_tactical_config);
4058
4059 let pattern_only_evaluation = engine.evaluate_position(&board);
4060 assert!(pattern_only_evaluation.is_some());
4061 }
4062}