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 strategic_evaluator;
80pub mod streaming_loader;
81pub mod tactical_search;
82pub mod training;
83pub mod ultra_fast_loader;
84pub mod variational_autoencoder;
85pub mod uci;
87
88pub use auto_discovery::{AutoDiscovery, FormatPriority, TrainingFile};
89pub use gpu_acceleration::{DeviceType, GPUAccelerator};
90pub use lichess_loader::LichessLoader;
91pub use lsh::LSH;
92pub use manifold_learner::ManifoldLearner;
93pub use nnue::{BlendStrategy, EvalStats, HybridEvaluator, NNUEConfig, NNUE};
94pub use opening_book::{OpeningBook, OpeningBookStats, OpeningEntry};
95pub use persistence::{Database, LSHTableData, PositionData};
96pub use position_encoder::PositionEncoder;
97pub use similarity_search::SimilaritySearch;
98pub use strategic_evaluator::{
99 AttackingPattern, PlanGoal, PlanUrgency, PositionalPlan, StrategicConfig, StrategicEvaluation,
100 StrategicEvaluator,
101};
102pub use streaming_loader::StreamingLoader;
103pub use tactical_search::{TacticalConfig, TacticalResult, TacticalSearch};
104pub use training::{
105 AdvancedSelfLearningSystem, EngineEvaluator, GameExtractor, LearningProgress, LearningStats,
106 SelfPlayConfig, SelfPlayTrainer, TacticalPuzzle, TacticalPuzzleParser, TacticalTrainingData,
107 TrainingData, TrainingDataset,
108};
109pub use ultra_fast_loader::{LoadingStats, UltraFastLoader};
110pub use variational_autoencoder::{VAEConfig, VariationalAutoencoder};
111pub use uci::{run_uci_engine, run_uci_engine_with_config, UCIConfig, UCIEngine};
113
114use chess::{Board, ChessMove};
115use ndarray::{Array1, Array2};
116use serde_json::Value;
117use std::collections::HashMap;
118use std::path::Path;
119use std::str::FromStr;
120
121fn move_centrality(chess_move: &ChessMove) -> f32 {
124 let dest_square = chess_move.get_dest();
125 let rank = dest_square.get_rank().to_index() as f32;
126 let file = dest_square.get_file().to_index() as f32;
127
128 let center_rank = 3.5;
130 let center_file = 3.5;
131
132 let rank_distance = (rank - center_rank).abs();
133 let file_distance = (file - center_file).abs();
134
135 let max_distance = 3.5; let distance = (rank_distance + file_distance) / 2.0;
138 max_distance - distance
139}
140
141#[derive(Debug, Clone)]
143pub struct MoveRecommendation {
144 pub chess_move: ChessMove,
145 pub confidence: f32,
146 pub from_similar_position_count: usize,
147 pub average_outcome: f32,
148}
149
150#[derive(Debug, Clone)]
152pub struct TrainingStats {
153 pub total_positions: usize,
154 pub unique_positions: usize,
155 pub has_move_data: bool,
156 pub move_data_entries: usize,
157 pub lsh_enabled: bool,
158 pub manifold_enabled: bool,
159 pub opening_book_enabled: bool,
160}
161
162#[derive(Debug, Clone)]
164pub struct HybridConfig {
165 pub pattern_confidence_threshold: f32,
167 pub enable_tactical_refinement: bool,
169 pub tactical_config: TacticalConfig,
171 pub pattern_weight: f32,
173 pub min_similar_positions: usize,
175}
176
177impl Default for HybridConfig {
178 fn default() -> Self {
179 Self {
180 pattern_confidence_threshold: 0.85, enable_tactical_refinement: true,
182 tactical_config: TacticalConfig::default(),
183 pattern_weight: 0.3, min_similar_positions: 5, }
186 }
187}
188
189pub struct ChessVectorEngine {
243 encoder: PositionEncoder,
244 similarity_search: SimilaritySearch,
245 lsh_index: Option<LSH>,
246 manifold_learner: Option<ManifoldLearner>,
247 use_lsh: bool,
248 use_manifold: bool,
249 position_moves: HashMap<usize, Vec<(ChessMove, f32)>>,
251 manifold_similarity_search: Option<SimilaritySearch>,
253 manifold_lsh_index: Option<LSH>,
255 position_vectors: Vec<Array1<f32>>,
257 position_boards: Vec<Board>,
259 position_evaluations: Vec<f32>,
261 opening_book: Option<OpeningBook>,
263 database: Option<Database>,
265 tactical_search: Option<TacticalSearch>,
267 hybrid_config: HybridConfig,
271 nnue: Option<NNUE>,
273 strategic_evaluator: Option<StrategicEvaluator>,
275}
276
277impl Clone for ChessVectorEngine {
278 fn clone(&self) -> Self {
279 Self {
280 encoder: self.encoder.clone(),
281 similarity_search: self.similarity_search.clone(),
282 lsh_index: self.lsh_index.clone(),
283 manifold_learner: None, use_lsh: self.use_lsh,
285 use_manifold: false, position_moves: self.position_moves.clone(),
287 manifold_similarity_search: self.manifold_similarity_search.clone(),
288 manifold_lsh_index: self.manifold_lsh_index.clone(),
289 position_vectors: self.position_vectors.clone(),
290 position_boards: self.position_boards.clone(),
291 position_evaluations: self.position_evaluations.clone(),
292 opening_book: self.opening_book.clone(),
293 database: None, tactical_search: self.tactical_search.clone(),
295 hybrid_config: self.hybrid_config.clone(),
297 nnue: None, strategic_evaluator: self.strategic_evaluator.clone(),
299 }
300 }
301}
302
303impl ChessVectorEngine {
304 pub fn new(vector_size: usize) -> Self {
306 let mut engine = Self {
307 encoder: PositionEncoder::new(vector_size),
308 similarity_search: SimilaritySearch::new(vector_size),
309 lsh_index: None,
310 manifold_learner: None,
311 use_lsh: false,
312 use_manifold: false,
313 position_moves: HashMap::new(),
314 manifold_similarity_search: None,
315 manifold_lsh_index: None,
316 position_vectors: Vec::new(),
317 position_boards: Vec::new(),
318 position_evaluations: Vec::new(),
319 opening_book: None,
320 database: None,
321 tactical_search: None,
322 hybrid_config: HybridConfig::default(),
324 nnue: None,
325 strategic_evaluator: None,
326 };
327
328 engine.enable_tactical_search_default();
330 engine
331 }
332
333 pub fn new_strong(vector_size: usize) -> Self {
335 let mut engine = Self::new(vector_size);
336 engine.enable_tactical_search(crate::tactical_search::TacticalConfig::strong());
338 engine
339 }
340
341 pub fn new_lightweight(vector_size: usize) -> Self {
343 Self {
344 encoder: PositionEncoder::new(vector_size),
345 similarity_search: SimilaritySearch::new(vector_size),
346 lsh_index: None,
347 manifold_learner: None,
348 use_lsh: false,
349 use_manifold: false,
350 position_moves: HashMap::new(),
351 manifold_similarity_search: None,
352 manifold_lsh_index: None,
353 position_vectors: Vec::new(),
354 position_boards: Vec::new(),
355 position_evaluations: Vec::new(),
356 opening_book: None,
357 database: None,
358 tactical_search: None, hybrid_config: HybridConfig::default(),
360 nnue: None,
361 strategic_evaluator: None,
362 }
363 }
364
365 pub fn new_adaptive(vector_size: usize, expected_positions: usize, use_case: &str) -> Self {
368 match use_case {
369 "training" => {
370 if expected_positions > 10000 {
371 Self::new_with_lsh(vector_size, 12, 20)
373 } else {
374 Self::new(vector_size)
375 }
376 }
377 "gameplay" => {
378 if expected_positions > 15000 {
379 Self::new_with_lsh(vector_size, 10, 18)
381 } else {
382 Self::new(vector_size)
383 }
384 }
385 "analysis" => {
386 if expected_positions > 10000 {
387 Self::new_with_lsh(vector_size, 14, 22)
389 } else {
390 Self::new(vector_size)
391 }
392 }
393 _ => Self::new(vector_size), }
395 }
396
397 pub fn new_with_lsh(vector_size: usize, num_tables: usize, hash_size: usize) -> Self {
399 Self {
400 encoder: PositionEncoder::new(vector_size),
401 similarity_search: SimilaritySearch::new(vector_size),
402 lsh_index: Some(LSH::new(vector_size, num_tables, hash_size)),
403 manifold_learner: None,
404 use_lsh: true,
405 use_manifold: false,
406 position_moves: HashMap::new(),
407 manifold_similarity_search: None,
408 manifold_lsh_index: None,
409 position_vectors: Vec::new(),
410 position_boards: Vec::new(),
411 position_evaluations: Vec::new(),
412 opening_book: None,
413 database: None,
414 tactical_search: None,
415 hybrid_config: HybridConfig::default(),
417 nnue: None,
418 strategic_evaluator: None,
419 }
420 }
421
422 pub fn enable_lsh(&mut self, num_tables: usize, hash_size: usize) {
424 self.lsh_index = Some(LSH::new(self.encoder.vector_size(), num_tables, hash_size));
425 self.use_lsh = true;
426
427 if let Some(ref mut lsh) = self.lsh_index {
429 for (vector, evaluation) in self.similarity_search.get_all_positions() {
430 lsh.add_vector(vector, evaluation);
431 }
432 }
433 }
434
435 pub fn add_position(&mut self, board: &Board, evaluation: f32) {
437 if !self.is_position_safe(board) {
439 return; }
441
442 let vector = self.encoder.encode(board);
443 self.similarity_search
444 .add_position(vector.clone(), evaluation);
445
446 self.position_vectors.push(vector.clone());
448 self.position_boards.push(*board);
449 self.position_evaluations.push(evaluation);
450
451 if let Some(ref mut lsh) = self.lsh_index {
453 lsh.add_vector(vector.clone(), evaluation);
454 }
455
456 if self.use_manifold {
458 if let Some(ref learner) = self.manifold_learner {
459 let compressed = learner.encode(&vector);
460
461 if let Some(ref mut search) = self.manifold_similarity_search {
462 search.add_position(compressed.clone(), evaluation);
463 }
464
465 if let Some(ref mut lsh) = self.manifold_lsh_index {
466 lsh.add_vector(compressed, evaluation);
467 }
468 }
469 }
470 }
471
472 pub fn find_similar_positions(&self, board: &Board, k: usize) -> Vec<(Array1<f32>, f32, f32)> {
474 let query_vector = self.encoder.encode(board);
475
476 if self.use_manifold {
478 if let Some(ref manifold_learner) = self.manifold_learner {
479 let compressed_query = manifold_learner.encode(&query_vector);
480
481 if let Some(ref lsh) = self.manifold_lsh_index {
483 return lsh.query(&compressed_query, k);
484 }
485
486 if let Some(ref search) = self.manifold_similarity_search {
488 return search.search(&compressed_query, k);
489 }
490 }
491 }
492
493 if self.use_lsh {
495 if let Some(ref lsh_index) = self.lsh_index {
496 return lsh_index.query(&query_vector, k);
497 }
498 }
499
500 self.similarity_search.search(&query_vector, k)
502 }
503
504 pub fn find_similar_positions_with_indices(
506 &self,
507 board: &Board,
508 k: usize,
509 ) -> Vec<(usize, f32, f32)> {
510 let query_vector = self.encoder.encode(board);
511
512 let mut results = Vec::new();
515
516 for (i, stored_vector) in self.position_vectors.iter().enumerate() {
517 let similarity = self.encoder.similarity(&query_vector, stored_vector);
518 let eval = self.position_evaluations.get(i).copied().unwrap_or(0.0);
519 results.push((i, eval, similarity));
520 }
521
522 results.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
524 results.truncate(k);
525
526 results
527 }
528
529 pub fn evaluate_position(&mut self, board: &Board) -> Option<f32> {
531 if let Some(entry) = self.get_opening_entry(board) {
540 return Some(entry.evaluation);
541 }
542
543 let nnue_evaluation = if let Some(ref mut nnue) = self.nnue {
545 nnue.evaluate(board).ok()
546 } else {
547 None
548 };
549
550 let similar_positions = self.find_similar_positions(board, 5);
552
553 if similar_positions.is_empty() {
554 if let Some(nnue_eval) = nnue_evaluation {
556 return Some(nnue_eval);
557 }
558
559 if let Some(ref mut tactical_search) = self.tactical_search {
560 let result = tactical_search.search(board);
561 return Some(result.evaluation);
562 }
563 return None;
564 }
565
566 let mut weighted_sum = 0.0;
568 let mut weight_sum = 0.0;
569 let mut similarity_scores = Vec::new();
570
571 for (_, evaluation, similarity) in &similar_positions {
572 let weight = *similarity;
573 weighted_sum += evaluation * weight;
574 weight_sum += weight;
575 similarity_scores.push(*similarity);
576 }
577
578 let pattern_evaluation = weighted_sum / weight_sum;
579
580 let avg_similarity = similarity_scores.iter().sum::<f32>() / similarity_scores.len() as f32;
582 let count_factor = (similar_positions.len() as f32
583 / self.hybrid_config.min_similar_positions as f32)
584 .min(1.0);
585 let pattern_confidence = avg_similarity * count_factor;
586
587 let use_tactical = self.hybrid_config.enable_tactical_refinement
589 && pattern_confidence < self.hybrid_config.pattern_confidence_threshold
590 && self.tactical_search.is_some();
591
592 if use_tactical {
593 if let Some(ref mut tactical_search) = self.tactical_search {
595 let tactical_result = if tactical_search.config.enable_parallel_search {
596 tactical_search.search_parallel(board)
597 } else {
598 tactical_search.search(board)
599 };
600
601 let mut hybrid_evaluation = pattern_evaluation;
603
604 if nnue_evaluation.is_some() {
606 if let Some(ref mut nnue) = self.nnue {
608 if let Ok(nnue_hybrid_eval) =
609 nnue.evaluate_hybrid(board, Some(pattern_evaluation))
610 {
611 hybrid_evaluation = nnue_hybrid_eval;
612 }
613 }
614 }
615
616 let pattern_weight = self.hybrid_config.pattern_weight * pattern_confidence;
618 let tactical_weight = 1.0 - pattern_weight;
619
620 hybrid_evaluation = (hybrid_evaluation * pattern_weight)
621 + (tactical_result.evaluation * tactical_weight);
622
623 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
625 hybrid_evaluation = strategic_evaluator.blend_with_hybrid_evaluation(
626 board,
627 nnue_evaluation.unwrap_or(hybrid_evaluation),
628 pattern_evaluation,
629 );
630 }
631
632 Some(hybrid_evaluation)
633 } else {
634 if nnue_evaluation.is_some() {
636 if let Some(ref mut nnue) = self.nnue {
637 nnue.evaluate_hybrid(board, Some(pattern_evaluation)).ok()
639 } else {
640 Some(pattern_evaluation)
641 }
642 } else {
643 Some(pattern_evaluation)
644 }
645 }
646 } else {
647 let mut final_evaluation = pattern_evaluation;
649
650 if nnue_evaluation.is_some() {
652 if let Some(ref mut nnue) = self.nnue {
653 if let Ok(nnue_hybrid_eval) =
655 nnue.evaluate_hybrid(board, Some(pattern_evaluation))
656 {
657 final_evaluation = nnue_hybrid_eval;
658 }
659 }
660 }
661
662 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
664 final_evaluation = strategic_evaluator.blend_with_hybrid_evaluation(
665 board,
666 nnue_evaluation.unwrap_or(0.0),
667 pattern_evaluation,
668 );
669 }
670
671 Some(final_evaluation)
672 }
673 }
674
675 pub fn encode_position(&self, board: &Board) -> Array1<f32> {
677 self.encoder.encode(board)
678 }
679
680 pub fn calculate_similarity(&self, board1: &Board, board2: &Board) -> f32 {
682 let vec1 = self.encoder.encode(board1);
683 let vec2 = self.encoder.encode(board2);
684 self.encoder.similarity(&vec1, &vec2)
685 }
686
687 pub fn knowledge_base_size(&self) -> usize {
689 self.similarity_search.size()
690 }
691
692 pub fn save_training_data<P: AsRef<std::path::Path>>(
694 &self,
695 path: P,
696 ) -> Result<(), Box<dyn std::error::Error>> {
697 use crate::training::{TrainingData, TrainingDataset};
698
699 let mut dataset = TrainingDataset::new();
700
701 for (i, board) in self.position_boards.iter().enumerate() {
703 if i < self.position_evaluations.len() {
704 dataset.data.push(TrainingData {
705 board: *board,
706 evaluation: self.position_evaluations[i],
707 depth: 15, game_id: i, });
710 }
711 }
712
713 dataset.save_incremental(path)?;
714 println!("Saved {} positions to training data", dataset.data.len());
715 Ok(())
716 }
717
718 pub fn load_training_data_incremental<P: AsRef<std::path::Path>>(
720 &mut self,
721 path: P,
722 ) -> Result<(), Box<dyn std::error::Error>> {
723 use crate::training::TrainingDataset;
724 use indicatif::{ProgressBar, ProgressStyle};
725 use std::collections::HashSet;
726
727 let existing_size = self.knowledge_base_size();
728
729 let path_ref = path.as_ref();
731 let binary_path = path_ref.with_extension("bin");
732 if binary_path.exists() {
733 println!("š Loading optimized binary format...");
734 return self.load_training_data_binary(binary_path);
735 }
736
737 println!("š Loading training data from {}...", path_ref.display());
738 let dataset = TrainingDataset::load(path)?;
739
740 let total_positions = dataset.data.len();
741 if total_positions == 0 {
742 println!("ā ļø No positions found in dataset");
743 return Ok(());
744 }
745
746 let dedup_pb = ProgressBar::new(total_positions as u64);
748 dedup_pb.set_style(
749 ProgressStyle::default_bar()
750 .template("š Checking duplicates [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")?
751 .progress_chars("āāā")
752 );
753
754 let mut existing_boards: HashSet<_> = self.position_boards.iter().cloned().collect();
756 let mut new_positions = Vec::new();
757 let mut new_evaluations = Vec::new();
758
759 for (i, data) in dataset.data.into_iter().enumerate() {
761 if !existing_boards.contains(&data.board) {
762 existing_boards.insert(data.board);
763 new_positions.push(data.board);
764 new_evaluations.push(data.evaluation);
765 }
766
767 if i % 1000 == 0 || i == total_positions - 1 {
768 dedup_pb.set_position((i + 1) as u64);
769 dedup_pb.set_message(format!("{} new positions found", new_positions.len()));
770 }
771 }
772 dedup_pb.finish_with_message(format!("ā
Found {} new positions", new_positions.len()));
773
774 if new_positions.is_empty() {
775 println!("ā¹ļø No new positions to add (all positions already exist)");
776 return Ok(());
777 }
778
779 let add_pb = ProgressBar::new(new_positions.len() as u64);
781 add_pb.set_style(
782 ProgressStyle::default_bar()
783 .template("ā Adding positions [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")?
784 .progress_chars("āāā")
785 );
786
787 for (i, (board, evaluation)) in new_positions
789 .into_iter()
790 .zip(new_evaluations.into_iter())
791 .enumerate()
792 {
793 self.add_position(&board, evaluation);
794
795 if i % 500 == 0 || i == add_pb.length().unwrap() as usize - 1 {
796 add_pb.set_position((i + 1) as u64);
797 add_pb.set_message("vectors encoded".to_string());
798 }
799 }
800 add_pb.finish_with_message("ā
All positions added");
801
802 println!(
803 "šÆ Loaded {} new positions (total: {})",
804 self.knowledge_base_size() - existing_size,
805 self.knowledge_base_size()
806 );
807 Ok(())
808 }
809
810 pub fn save_training_data_binary<P: AsRef<std::path::Path>>(
812 &self,
813 path: P,
814 ) -> Result<(), Box<dyn std::error::Error>> {
815 use lz4_flex::compress_prepend_size;
816
817 println!("š¾ Saving training data in binary format (compressed)...");
818
819 #[derive(serde::Serialize)]
821 struct BinaryTrainingData {
822 positions: Vec<String>, evaluations: Vec<f32>,
824 vectors: Vec<Vec<f32>>, created_at: i64,
826 }
827
828 let current_time = std::time::SystemTime::now()
829 .duration_since(std::time::UNIX_EPOCH)?
830 .as_secs() as i64;
831
832 let mut positions = Vec::with_capacity(self.position_boards.len());
834 let mut evaluations = Vec::with_capacity(self.position_boards.len());
835 let mut vectors = Vec::with_capacity(self.position_boards.len());
836
837 for (i, board) in self.position_boards.iter().enumerate() {
838 if i < self.position_evaluations.len() {
839 positions.push(board.to_string());
840 evaluations.push(self.position_evaluations[i]);
841
842 if i < self.position_vectors.len() {
844 if let Some(vector_slice) = self.position_vectors[i].as_slice() {
845 vectors.push(vector_slice.to_vec());
846 }
847 }
848 }
849 }
850
851 let binary_data = BinaryTrainingData {
852 positions,
853 evaluations,
854 vectors,
855 created_at: current_time,
856 };
857
858 let serialized = bincode::serialize(&binary_data)?;
860
861 let compressed = compress_prepend_size(&serialized);
863
864 std::fs::write(path, &compressed)?;
866
867 println!(
868 "ā
Saved {} positions to binary file ({} bytes compressed)",
869 binary_data.positions.len(),
870 compressed.len()
871 );
872 Ok(())
873 }
874
875 pub fn load_training_data_binary<P: AsRef<std::path::Path>>(
877 &mut self,
878 path: P,
879 ) -> Result<(), Box<dyn std::error::Error>> {
880 use indicatif::{ProgressBar, ProgressStyle};
881 use lz4_flex::decompress_size_prepended;
882 use rayon::prelude::*;
883
884 println!("š Loading training data from binary format...");
885
886 #[derive(serde::Deserialize)]
887 struct BinaryTrainingData {
888 positions: Vec<String>,
889 evaluations: Vec<f32>,
890 #[allow(dead_code)]
891 vectors: Vec<Vec<f32>>,
892 #[allow(dead_code)]
893 created_at: i64,
894 }
895
896 let existing_size = self.knowledge_base_size();
897
898 let file_size = std::fs::metadata(&path)?.len();
900 println!(
901 "š¦ Reading {} compressed file...",
902 Self::format_bytes(file_size)
903 );
904
905 let compressed_data = std::fs::read(path)?;
906 println!("š Decompressing data...");
907 let serialized = decompress_size_prepended(&compressed_data)?;
908
909 println!("š Deserializing binary data...");
910 let binary_data: BinaryTrainingData = bincode::deserialize(&serialized)?;
911
912 let total_positions = binary_data.positions.len();
913 if total_positions == 0 {
914 println!("ā ļø No positions found in binary file");
915 return Ok(());
916 }
917
918 println!("š Processing {total_positions} positions from binary format...");
919
920 let pb = ProgressBar::new(total_positions as u64);
922 pb.set_style(
923 ProgressStyle::default_bar()
924 .template("ā” Loading positions [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")?
925 .progress_chars("āāā")
926 );
927
928 let mut added_count = 0;
929
930 if total_positions > 10_000 {
932 println!("š Using parallel batch processing for large dataset...");
933
934 let existing_positions: std::collections::HashSet<_> =
936 self.position_boards.iter().cloned().collect();
937
938 let batch_size = 5000.min(total_positions / num_cpus::get()).max(1000);
940 let batches: Vec<_> = binary_data
941 .positions
942 .chunks(batch_size)
943 .zip(binary_data.evaluations.chunks(batch_size))
944 .collect();
945
946 println!(
947 "š Processing {} batches of ~{} positions each...",
948 batches.len(),
949 batch_size
950 );
951
952 let valid_positions: Vec<Vec<(Board, f32)>> = batches
954 .par_iter()
955 .map(|(fen_batch, eval_batch)| {
956 let mut batch_positions = Vec::new();
957
958 for (fen, &evaluation) in fen_batch.iter().zip(eval_batch.iter()) {
959 if let Ok(board) = fen.parse::<Board>() {
960 if !existing_positions.contains(&board) {
961 let mut eval = evaluation;
962 if eval.abs() > 15.0 {
964 eval /= 100.0;
965 }
966 batch_positions.push((board, eval));
967 }
968 }
969 }
970
971 batch_positions
972 })
973 .collect();
974
975 for batch in valid_positions {
977 for (board, evaluation) in batch {
978 self.add_position(&board, evaluation);
979 added_count += 1;
980
981 if added_count % 1000 == 0 {
982 pb.set_position(added_count as u64);
983 pb.set_message(format!("{added_count} new positions"));
984 }
985 }
986 }
987 } else {
988 for (i, fen) in binary_data.positions.iter().enumerate() {
990 if i < binary_data.evaluations.len() {
991 if let Ok(board) = fen.parse() {
992 if !self.position_boards.contains(&board) {
994 let mut evaluation = binary_data.evaluations[i];
995
996 if evaluation.abs() > 15.0 {
998 evaluation /= 100.0;
999 }
1000
1001 self.add_position(&board, evaluation);
1002 added_count += 1;
1003 }
1004 }
1005 }
1006
1007 if i % 1000 == 0 || i == total_positions - 1 {
1008 pb.set_position((i + 1) as u64);
1009 pb.set_message(format!("{added_count} new positions"));
1010 }
1011 }
1012 }
1013 pb.finish_with_message(format!("ā
Loaded {added_count} new positions"));
1014
1015 println!(
1016 "šÆ Binary loading complete: {} new positions (total: {})",
1017 self.knowledge_base_size() - existing_size,
1018 self.knowledge_base_size()
1019 );
1020 Ok(())
1021 }
1022
1023 pub fn load_training_data_mmap<P: AsRef<Path>>(
1026 &mut self,
1027 path: P,
1028 ) -> Result<(), Box<dyn std::error::Error>> {
1029 use memmap2::Mmap;
1030 use std::fs::File;
1031
1032 let path_ref = path.as_ref();
1033 println!(
1034 "š Loading training data via memory mapping: {}",
1035 path_ref.display()
1036 );
1037
1038 let file = File::open(path_ref)?;
1039 let mmap = unsafe { Mmap::map(&file)? };
1040
1041 if let Ok(data) = rmp_serde::from_slice::<Vec<(String, f32)>>(&mmap) {
1043 println!("š¦ Detected MessagePack format");
1044 return self.load_positions_from_tuples(data);
1045 }
1046
1047 if let Ok(data) = bincode::deserialize::<Vec<(String, f32)>>(&mmap) {
1049 println!("š¦ Detected bincode format");
1050 return self.load_positions_from_tuples(data);
1051 }
1052
1053 let decompressed = lz4_flex::decompress_size_prepended(&mmap)?;
1055 let data: Vec<(String, f32)> = bincode::deserialize(&decompressed)?;
1056 println!("š¦ Detected LZ4+bincode format");
1057 self.load_positions_from_tuples(data)
1058 }
1059
1060 pub fn load_training_data_msgpack<P: AsRef<Path>>(
1063 &mut self,
1064 path: P,
1065 ) -> Result<(), Box<dyn std::error::Error>> {
1066 use std::fs::File;
1067 use std::io::BufReader;
1068
1069 let path_ref = path.as_ref();
1070 println!(
1071 "š Loading MessagePack training data: {}",
1072 path_ref.display()
1073 );
1074
1075 let file = File::open(path_ref)?;
1076 let reader = BufReader::new(file);
1077 let data: Vec<(String, f32)> = rmp_serde::from_read(reader)?;
1078
1079 println!("š¦ MessagePack data loaded: {} positions", data.len());
1080 self.load_positions_from_tuples(data)
1081 }
1082
1083 pub fn load_training_data_streaming_json<P: AsRef<Path>>(
1086 &mut self,
1087 path: P,
1088 ) -> Result<(), Box<dyn std::error::Error>> {
1089 use dashmap::DashMap;
1090 use rayon::prelude::*;
1091 use std::fs::File;
1092 use std::io::{BufRead, BufReader};
1093 use std::sync::Arc;
1094
1095 let path_ref = path.as_ref();
1096 println!(
1097 "š Loading JSON with streaming parallel processing: {}",
1098 path_ref.display()
1099 );
1100
1101 let file = File::open(path_ref)?;
1102 let reader = BufReader::new(file);
1103
1104 let chunk_size = 10000;
1106 let position_map = Arc::new(DashMap::new());
1107
1108 let lines: Vec<String> = reader.lines().collect::<Result<Vec<_>, _>>()?;
1109 let total_lines = lines.len();
1110
1111 lines.par_chunks(chunk_size).for_each(|chunk| {
1113 for line in chunk {
1114 if let Ok(data) = serde_json::from_str::<serde_json::Value>(line) {
1115 if let (Some(fen), Some(eval)) = (
1116 data.get("fen").and_then(|v| v.as_str()),
1117 data.get("evaluation").and_then(|v| v.as_f64()),
1118 ) {
1119 position_map.insert(fen.to_string(), eval as f32);
1120 }
1121 }
1122 }
1123 });
1124
1125 println!(
1126 "š¦ Parallel JSON processing complete: {} positions from {} lines",
1127 position_map.len(),
1128 total_lines
1129 );
1130
1131 let data: Vec<(String, f32)> = match Arc::try_unwrap(position_map) {
1134 Ok(map) => map.into_iter().collect(),
1135 Err(arc_map) => {
1136 arc_map
1138 .iter()
1139 .map(|entry| (entry.key().clone(), *entry.value()))
1140 .collect()
1141 }
1142 };
1143 self.load_positions_from_tuples(data)
1144 }
1145
1146 pub fn load_training_data_compressed<P: AsRef<Path>>(
1149 &mut self,
1150 path: P,
1151 ) -> Result<(), Box<dyn std::error::Error>> {
1152 use std::fs::File;
1153 use std::io::BufReader;
1154
1155 let path_ref = path.as_ref();
1156 println!(
1157 "š Loading zstd compressed training data: {}",
1158 path_ref.display()
1159 );
1160
1161 let file = File::open(path_ref)?;
1162 let reader = BufReader::new(file);
1163 let decoder = zstd::stream::Decoder::new(reader)?;
1164
1165 if let Ok(data) = rmp_serde::from_read::<_, Vec<(String, f32)>>(decoder) {
1167 println!("š¦ Zstd+MessagePack data loaded: {} positions", data.len());
1168 return self.load_positions_from_tuples(data);
1169 }
1170
1171 let file = File::open(path_ref)?;
1173 let reader = BufReader::new(file);
1174 let decoder = zstd::stream::Decoder::new(reader)?;
1175 let data: Vec<(String, f32)> = bincode::deserialize_from(decoder)?;
1176
1177 println!("š¦ Zstd+bincode data loaded: {} positions", data.len());
1178 self.load_positions_from_tuples(data)
1179 }
1180
1181 fn load_positions_from_tuples(
1184 &mut self,
1185 data: Vec<(String, f32)>,
1186 ) -> Result<(), Box<dyn std::error::Error>> {
1187 use indicatif::{ProgressBar, ProgressStyle};
1188 use std::collections::HashSet;
1189
1190 let existing_size = self.knowledge_base_size();
1191 let mut seen_positions = HashSet::new();
1192 let mut loaded_count = 0;
1193
1194 let pb = ProgressBar::new(data.len() as u64);
1196 pb.set_style(ProgressStyle::with_template(
1197 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({per_sec}) {msg}"
1198 )?);
1199
1200 for (fen, evaluation) in data {
1201 pb.inc(1);
1202
1203 if seen_positions.contains(&fen) {
1205 continue;
1206 }
1207 seen_positions.insert(fen.clone());
1208
1209 if let Ok(board) = Board::from_str(&fen) {
1211 self.add_position(&board, evaluation);
1212 loaded_count += 1;
1213
1214 if loaded_count % 1000 == 0 {
1215 pb.set_message(format!("Loaded {loaded_count} positions"));
1216 }
1217 }
1218 }
1219
1220 pb.finish_with_message(format!("ā
Loaded {loaded_count} new positions"));
1221
1222 println!(
1223 "šÆ Ultra-fast loading complete: {} new positions (total: {})",
1224 self.knowledge_base_size() - existing_size,
1225 self.knowledge_base_size()
1226 );
1227
1228 Ok(())
1229 }
1230
1231 fn format_bytes(bytes: u64) -> String {
1233 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
1234 let mut size = bytes as f64;
1235 let mut unit_index = 0;
1236
1237 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
1238 size /= 1024.0;
1239 unit_index += 1;
1240 }
1241
1242 format!("{:.1} {}", size, UNITS[unit_index])
1243 }
1244
1245 pub fn train_from_dataset_incremental(&mut self, dataset: &crate::training::TrainingDataset) {
1247 let _existing_size = self.knowledge_base_size();
1248 let mut added = 0;
1249
1250 for data in &dataset.data {
1251 if !self.position_boards.contains(&data.board) {
1253 self.add_position(&data.board, data.evaluation);
1254 added += 1;
1255 }
1256 }
1257
1258 println!(
1259 "Added {} new positions from dataset (total: {})",
1260 added,
1261 self.knowledge_base_size()
1262 );
1263 }
1264
1265 pub fn training_stats(&self) -> TrainingStats {
1267 TrainingStats {
1268 total_positions: self.knowledge_base_size(),
1269 unique_positions: self.position_boards.len(),
1270 has_move_data: !self.position_moves.is_empty(),
1271 move_data_entries: self.position_moves.len(),
1272 lsh_enabled: self.use_lsh,
1273 manifold_enabled: self.use_manifold,
1274 opening_book_enabled: self.opening_book.is_some(),
1275 }
1276 }
1277
1278 pub fn auto_load_training_data(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
1280 use indicatif::{ProgressBar, ProgressStyle};
1281
1282 let common_files = vec![
1283 "training_data.json",
1284 "tactical_training_data.json",
1285 "engine_training.json",
1286 "chess_training.json",
1287 "my_training.json",
1288 ];
1289
1290 let tactical_files = vec![
1291 "tactical_puzzles.json",
1292 "lichess_puzzles.json",
1293 "my_puzzles.json",
1294 ];
1295
1296 let mut available_files = Vec::new();
1298 for file_path in &common_files {
1299 if std::path::Path::new(file_path).exists() {
1300 available_files.push((file_path, "training"));
1301 }
1302 }
1303 for file_path in &tactical_files {
1304 if std::path::Path::new(file_path).exists() {
1305 available_files.push((file_path, "tactical"));
1306 }
1307 }
1308
1309 if available_files.is_empty() {
1310 return Ok(Vec::new());
1311 }
1312
1313 println!(
1314 "š Found {} training files to auto-load",
1315 available_files.len()
1316 );
1317
1318 let pb = ProgressBar::new(available_files.len() as u64);
1320 pb.set_style(
1321 ProgressStyle::default_bar()
1322 .template("š Auto-loading files [{elapsed_precise}] [{bar:40.blue/cyan}] {pos}/{len} {msg}")?
1323 .progress_chars("āāā")
1324 );
1325
1326 let mut loaded_files = Vec::new();
1327
1328 for (i, (file_path, file_type)) in available_files.iter().enumerate() {
1329 pb.set_position(i as u64);
1330 pb.set_message("Processing...".to_string());
1331
1332 let result = match *file_type {
1333 "training" => self.load_training_data_incremental(file_path).map(|_| {
1334 loaded_files.push(file_path.to_string());
1335 println!("Loading complete");
1336 }),
1337 "tactical" => crate::training::TacticalPuzzleParser::load_tactical_puzzles(
1338 file_path,
1339 )
1340 .map(|puzzles| {
1341 crate::training::TacticalPuzzleParser::load_into_engine_incremental(
1342 &puzzles, self,
1343 );
1344 loaded_files.push(file_path.to_string());
1345 println!("Loading complete");
1346 }),
1347 _ => Ok(()),
1348 };
1349
1350 if let Err(_e) = result {
1351 println!("Loading complete");
1352 }
1353 }
1354
1355 pb.set_position(available_files.len() as u64);
1356 pb.finish_with_message(format!("ā
Auto-loaded {} files", loaded_files.len()));
1357
1358 Ok(loaded_files)
1359 }
1360
1361 pub fn load_lichess_puzzles<P: AsRef<std::path::Path>>(
1363 &mut self,
1364 csv_path: P,
1365 ) -> Result<(), Box<dyn std::error::Error>> {
1366 println!("š„ Loading Lichess puzzles with enhanced performance...");
1367 let puzzle_entries =
1368 crate::lichess_loader::load_lichess_puzzles_basic_with_moves(csv_path, 100000)?;
1369
1370 for (board, evaluation, best_move) in puzzle_entries {
1371 self.add_position_with_move(&board, evaluation, Some(best_move), Some(evaluation));
1372 }
1373
1374 println!("ā
Lichess puzzle loading complete!");
1375 Ok(())
1376 }
1377
1378 pub fn load_lichess_puzzles_with_limit<P: AsRef<std::path::Path>>(
1380 &mut self,
1381 csv_path: P,
1382 max_puzzles: Option<usize>,
1383 ) -> Result<(), Box<dyn std::error::Error>> {
1384 match max_puzzles {
1385 Some(limit) => {
1386 println!("š Loading Lichess puzzles (limited to {limit} puzzles)...");
1387 let puzzle_entries =
1388 crate::lichess_loader::load_lichess_puzzles_basic_with_moves(csv_path, limit)?;
1389
1390 for (board, evaluation, best_move) in puzzle_entries {
1391 self.add_position_with_move(
1392 &board,
1393 evaluation,
1394 Some(best_move),
1395 Some(evaluation),
1396 );
1397 }
1398 }
1399 None => {
1400 self.load_lichess_puzzles(csv_path)?;
1402 return Ok(());
1403 }
1404 }
1405
1406 println!("ā
Lichess puzzle loading complete!");
1407 Ok(())
1408 }
1409
1410 pub fn new_with_auto_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1412 let mut engine = Self::new(vector_size);
1413 engine.enable_opening_book();
1414
1415 let loaded_files = engine.auto_load_training_data()?;
1417
1418 if loaded_files.is_empty() {
1419 println!("š¤ Created fresh engine (no training data found)");
1420 } else {
1421 println!(
1422 "š Created engine with auto-loaded training data from {} files",
1423 loaded_files.len()
1424 );
1425 let _stats = engine.training_stats();
1426 println!("Loading complete");
1427 println!("Loading complete");
1428 }
1429
1430 Ok(engine)
1431 }
1432
1433 pub fn new_with_fast_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1436 use indicatif::{ProgressBar, ProgressStyle};
1437
1438 let mut engine = Self::new(vector_size);
1439 engine.enable_opening_book();
1440
1441 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
1443 println!("Loading complete");
1444 }
1445
1446 let binary_files = [
1448 "training_data_a100.bin", "training_data.bin",
1450 "tactical_training_data.bin",
1451 "engine_training.bin",
1452 "chess_training.bin",
1453 ];
1454
1455 let existing_binary_files: Vec<_> = binary_files
1457 .iter()
1458 .filter(|&file_path| std::path::Path::new(file_path).exists())
1459 .collect();
1460
1461 let mut loaded_count = 0;
1462
1463 if !existing_binary_files.is_empty() {
1464 println!(
1465 "ā” Fast loading: Found {} binary files",
1466 existing_binary_files.len()
1467 );
1468
1469 let pb = ProgressBar::new(existing_binary_files.len() as u64);
1471 pb.set_style(
1472 ProgressStyle::default_bar()
1473 .template("š Fast loading [{elapsed_precise}] [{bar:40.green/cyan}] {pos}/{len} {msg}")?
1474 .progress_chars("āāā")
1475 );
1476
1477 for (i, file_path) in existing_binary_files.iter().enumerate() {
1478 pb.set_position(i as u64);
1479 pb.set_message("Processing...".to_string());
1480
1481 if engine.load_training_data_binary(file_path).is_ok() {
1482 loaded_count += 1;
1483 }
1484 }
1485
1486 pb.set_position(existing_binary_files.len() as u64);
1487 pb.finish_with_message(format!("ā
Loaded {loaded_count} binary files"));
1488 } else {
1489 println!("š¦ No binary files found, falling back to JSON auto-loading...");
1490 let _ = engine.auto_load_training_data()?;
1491 }
1492
1493 if let Err(e) = engine.load_manifold_models() {
1495 println!("ā ļø No pre-trained manifold models found ({e})");
1496 println!(" Use --rebuild-models flag to train new models");
1497 }
1498
1499 let stats = engine.training_stats();
1500 println!(
1501 "ā” Fast engine ready with {} positions ({} binary files loaded)",
1502 stats.total_positions, loaded_count
1503 );
1504
1505 Ok(engine)
1506 }
1507
1508 pub fn new_with_auto_discovery(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1511 println!("š Initializing engine with AUTO-DISCOVERY and format consolidation...");
1512 let mut engine = Self::new(vector_size);
1513 engine.enable_opening_book();
1514
1515 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
1517 println!("Loading complete");
1518 }
1519
1520 let discovered_files = AutoDiscovery::discover_training_files(".", true)?;
1522
1523 if discovered_files.is_empty() {
1524 println!("ā¹ļø No training data found. Use convert methods to create optimized files.");
1525 return Ok(engine);
1526 }
1527
1528 let consolidated = AutoDiscovery::consolidate_by_base_name(discovered_files.clone());
1530
1531 let mut total_loaded = 0;
1532 for (base_name, best_file) in &consolidated {
1533 println!("š Loading {} ({})", base_name, best_file.format);
1534
1535 let initial_size = engine.knowledge_base_size();
1536 engine.load_file_by_format(&best_file.path, &best_file.format)?;
1537 let loaded_count = engine.knowledge_base_size() - initial_size;
1538 total_loaded += loaded_count;
1539
1540 println!(" ā
Loaded {loaded_count} positions");
1541 }
1542
1543 let cleanup_candidates = AutoDiscovery::get_cleanup_candidates(&discovered_files);
1545 if !cleanup_candidates.is_empty() {
1546 println!(
1547 "š§¹ Found {} old format files that can be cleaned up:",
1548 cleanup_candidates.len()
1549 );
1550 AutoDiscovery::cleanup_old_formats(&cleanup_candidates, true)?; println!(" š” To actually remove old files, run: cargo run --bin cleanup_formats");
1553 }
1554
1555 if let Err(e) = engine.load_manifold_models() {
1557 println!("ā ļø No pre-trained manifold models found ({e})");
1558 }
1559
1560 println!(
1561 "šÆ Engine ready: {} positions loaded from {} datasets",
1562 total_loaded,
1563 consolidated.len()
1564 );
1565 Ok(engine)
1566 }
1567
1568 pub fn new_with_instant_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
1571 println!("š Initializing engine with INSTANT loading...");
1572 let mut engine = Self::new(vector_size);
1573 engine.enable_opening_book();
1574
1575 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
1577 println!("Loading complete");
1578 }
1579
1580 let discovered_files = AutoDiscovery::discover_training_files(".", false)?;
1582
1583 if discovered_files.is_empty() {
1584 println!("ā¹ļø No user training data found, loading starter dataset...");
1586 if let Err(_e) = engine.load_starter_dataset() {
1587 println!("Loading complete");
1588 println!("ā¹ļø Starting with empty engine");
1589 } else {
1590 println!(
1591 "ā
Loaded starter dataset with {} positions",
1592 engine.knowledge_base_size()
1593 );
1594 }
1595 return Ok(engine);
1596 }
1597
1598 if let Some(best_file) = discovered_files.first() {
1600 println!(
1601 "ā” Loading {} format: {}",
1602 best_file.format,
1603 best_file.path.display()
1604 );
1605 engine.load_file_by_format(&best_file.path, &best_file.format)?;
1606 println!(
1607 "ā
Loaded {} positions from {} format",
1608 engine.knowledge_base_size(),
1609 best_file.format
1610 );
1611 }
1612
1613 if let Err(e) = engine.load_manifold_models() {
1615 println!("ā ļø No pre-trained manifold models found ({e})");
1616 }
1617
1618 println!(
1619 "šÆ Engine ready: {} positions loaded",
1620 engine.knowledge_base_size()
1621 );
1622 Ok(engine)
1623 }
1624
1625 fn is_position_safe(&self, board: &Board) -> bool {
1630 match std::panic::catch_unwind(|| {
1632 use chess::MoveGen;
1633 let _legal_moves: Vec<ChessMove> = MoveGen::new_legal(board).collect();
1634 true
1635 }) {
1636 Ok(_) => true,
1637 Err(_) => {
1638 false
1640 }
1641 }
1642 }
1643
1644 pub fn check_gpu_acceleration(&self) -> Result<(), Box<dyn std::error::Error>> {
1646 match crate::gpu_acceleration::GPUAccelerator::new() {
1648 Ok(_) => {
1649 println!("š„ GPU acceleration available and ready");
1650 Ok(())
1651 }
1652 Err(_e) => Err("Processing...".to_string().into()),
1653 }
1654 }
1655
1656 pub fn load_starter_dataset(&mut self) -> Result<(), Box<dyn std::error::Error>> {
1658 let starter_data = if let Ok(file_content) =
1660 std::fs::read_to_string("training_data/starter_dataset.json")
1661 {
1662 file_content
1663 } else {
1664 r#"[
1666 {
1667 "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
1668 "evaluation": 0.0,
1669 "best_move": null,
1670 "depth": 0
1671 },
1672 {
1673 "fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
1674 "evaluation": 0.1,
1675 "best_move": "e7e5",
1676 "depth": 2
1677 },
1678 {
1679 "fen": "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
1680 "evaluation": 0.0,
1681 "best_move": "g1f3",
1682 "depth": 2
1683 }
1684 ]"#
1685 .to_string()
1686 };
1687
1688 let training_data: Vec<serde_json::Value> = serde_json::from_str(&starter_data)?;
1689
1690 for entry in training_data {
1691 if let (Some(fen), Some(evaluation)) = (entry.get("fen"), entry.get("evaluation")) {
1692 if let (Some(fen_str), Some(eval_f64)) = (fen.as_str(), evaluation.as_f64()) {
1693 match chess::Board::from_str(fen_str) {
1694 Ok(board) => {
1695 let mut eval = eval_f64 as f32;
1697
1698 if eval.abs() > 15.0 {
1701 eval /= 100.0;
1702 }
1703
1704 self.add_position(&board, eval);
1705 }
1706 Err(_) => {
1707 continue;
1709 }
1710 }
1711 }
1712 }
1713 }
1714
1715 Ok(())
1716 }
1717
1718 fn load_file_by_format(
1720 &mut self,
1721 path: &std::path::Path,
1722 format: &str,
1723 ) -> Result<(), Box<dyn std::error::Error>> {
1724 let file_size = std::fs::metadata(path)?.len();
1726
1727 if file_size > 10_000_000 {
1729 println!(
1730 "š Large file detected ({:.1} MB) - using ultra-fast loader",
1731 file_size as f64 / 1_000_000.0
1732 );
1733 return self.ultra_fast_load_any_format(path);
1734 }
1735
1736 match format {
1738 "MMAP" => self.load_training_data_mmap(path),
1739 "MSGPACK" => self.load_training_data_msgpack(path),
1740 "BINARY" => self.load_training_data_streaming_binary(path),
1741 "ZSTD" => self.load_training_data_compressed(path),
1742 "JSON" => self.load_training_data_streaming_json_v2(path),
1743 _ => Err("Processing...".to_string().into()),
1744 }
1745 }
1746
1747 pub fn ultra_fast_load_any_format<P: AsRef<std::path::Path>>(
1749 &mut self,
1750 path: P,
1751 ) -> Result<(), Box<dyn std::error::Error>> {
1752 let mut loader = UltraFastLoader::new_for_massive_datasets();
1753 loader.ultra_load_binary(path, self)?;
1754
1755 let stats = loader.get_stats();
1756 println!("š Ultra-fast loading complete:");
1757 println!(" ā
Loaded: {} positions", stats.loaded);
1758 println!("Loading complete");
1759 println!("Loading complete");
1760 println!(" š Success rate: {:.1}%", stats.success_rate() * 100.0);
1761
1762 Ok(())
1763 }
1764
1765 pub fn load_training_data_streaming_binary<P: AsRef<std::path::Path>>(
1768 &mut self,
1769 path: P,
1770 ) -> Result<(), Box<dyn std::error::Error>> {
1771 let mut loader = StreamingLoader::new();
1772 loader.stream_load_binary(path, self)?;
1773
1774 println!("š Streaming binary load complete:");
1775 println!(" Loaded: {} new positions", loader.loaded_count);
1776 println!("Loading complete");
1777 println!("Loading complete");
1778
1779 Ok(())
1780 }
1781
1782 pub fn load_training_data_streaming_json_v2<P: AsRef<std::path::Path>>(
1785 &mut self,
1786 path: P,
1787 ) -> Result<(), Box<dyn std::error::Error>> {
1788 let mut loader = StreamingLoader::new();
1789
1790 let batch_size = if std::fs::metadata(path.as_ref())?.len() > 100_000_000 {
1792 20000 } else {
1795 5000 };
1797
1798 loader.stream_load_json(path, self, batch_size)?;
1799
1800 println!("š Streaming JSON load complete:");
1801 println!(" Loaded: {} new positions", loader.loaded_count);
1802 println!("Loading complete");
1803 println!("Loading complete");
1804
1805 Ok(())
1806 }
1807
1808 pub fn new_for_massive_datasets(
1811 vector_size: usize,
1812 ) -> Result<Self, Box<dyn std::error::Error>> {
1813 println!("š Initializing engine for MASSIVE datasets (100k-1M+ positions)...");
1814 let mut engine = Self::new(vector_size);
1815 engine.enable_opening_book();
1816
1817 let discovered_files = AutoDiscovery::discover_training_files(".", false)?;
1819
1820 if discovered_files.is_empty() {
1821 println!("ā¹ļø No training data found");
1822 return Ok(engine);
1823 }
1824
1825 let largest_file = discovered_files
1827 .iter()
1828 .max_by_key(|f| f.size_bytes)
1829 .unwrap();
1830
1831 println!(
1832 "šÆ Loading largest dataset: {} ({} bytes)",
1833 largest_file.path.display(),
1834 largest_file.size_bytes
1835 );
1836
1837 engine.ultra_fast_load_any_format(&largest_file.path)?;
1839
1840 println!(
1841 "šÆ Engine ready: {} positions loaded",
1842 engine.knowledge_base_size()
1843 );
1844 Ok(engine)
1845 }
1846
1847 pub fn convert_to_msgpack() -> Result<(), Box<dyn std::error::Error>> {
1850 use serde_json::Value;
1851 use std::fs::File;
1852 use std::io::{BufReader, BufWriter};
1853
1854 if std::path::Path::new("training_data_a100.bin").exists() {
1856 Self::convert_a100_binary_to_json()?;
1857 }
1858
1859 let input_files = [
1860 "training_data.json",
1861 "tactical_training_data.json",
1862 "training_data_a100.json",
1863 ];
1864
1865 for input_file in &input_files {
1866 let input_path = std::path::Path::new(input_file);
1867 if !input_path.exists() {
1868 continue;
1869 }
1870
1871 let output_file_path = input_file.replace(".json", ".msgpack");
1872 println!("š Converting {input_file} ā {output_file_path} (MessagePack format)");
1873
1874 let file = File::open(input_path)?;
1876 let reader = BufReader::new(file);
1877 let json_value: Value = serde_json::from_reader(reader)?;
1878
1879 let data: Vec<(String, f32)> = match json_value {
1880 Value::Array(arr) if !arr.is_empty() => {
1882 if let Some(first) = arr.first() {
1883 if first.is_array() {
1884 arr.into_iter()
1886 .filter_map(|item| {
1887 if let Value::Array(tuple) = item {
1888 if tuple.len() >= 2 {
1889 let fen = tuple[0].as_str()?.to_string();
1890 let mut eval = tuple[1].as_f64()? as f32;
1891
1892 if eval.abs() > 15.0 {
1896 eval /= 100.0;
1897 }
1898
1899 Some((fen, eval))
1900 } else {
1901 None
1902 }
1903 } else {
1904 None
1905 }
1906 })
1907 .collect()
1908 } else if first.is_object() {
1909 arr.into_iter()
1911 .filter_map(|item| {
1912 if let Value::Object(obj) = item {
1913 let fen = obj.get("fen")?.as_str()?.to_string();
1914 let mut eval = obj.get("evaluation")?.as_f64()? as f32;
1915
1916 if eval.abs() > 15.0 {
1920 eval /= 100.0;
1921 }
1922
1923 Some((fen, eval))
1924 } else {
1925 None
1926 }
1927 })
1928 .collect()
1929 } else {
1930 return Err("Processing...".to_string().into());
1931 }
1932 } else {
1933 Vec::new()
1934 }
1935 }
1936 _ => return Err("Processing...".to_string().into()),
1937 };
1938
1939 if data.is_empty() {
1940 println!("Loading complete");
1941 continue;
1942 }
1943
1944 let output_file = File::create(&output_file_path)?;
1946 let mut writer = BufWriter::new(output_file);
1947 rmp_serde::encode::write(&mut writer, &data)?;
1948
1949 let input_size = input_path.metadata()?.len();
1950 let output_size = std::path::Path::new(&output_file_path).metadata()?.len();
1951 let ratio = input_size as f64 / output_size as f64;
1952
1953 println!(
1954 "ā
Converted: {} ā {} ({:.1}x size reduction, {} positions)",
1955 Self::format_bytes(input_size),
1956 Self::format_bytes(output_size),
1957 ratio,
1958 data.len()
1959 );
1960 }
1961
1962 Ok(())
1963 }
1964
1965 pub fn convert_a100_binary_to_json() -> Result<(), Box<dyn std::error::Error>> {
1967 use std::fs::File;
1968 use std::io::BufWriter;
1969
1970 let binary_path = "training_data_a100.bin";
1971 let json_path = "training_data_a100.json";
1972
1973 if !std::path::Path::new(binary_path).exists() {
1974 println!("Loading complete");
1975 return Ok(());
1976 }
1977
1978 println!("š Converting A100 binary data {binary_path} ā {json_path} (JSON format)");
1979
1980 let mut engine = ChessVectorEngine::new(1024);
1982 engine.load_training_data_binary(binary_path)?;
1983
1984 let mut data = Vec::new();
1986 for (i, board) in engine.position_boards.iter().enumerate() {
1987 if i < engine.position_evaluations.len() {
1988 data.push(serde_json::json!({
1989 "fen": board.to_string(),
1990 "evaluation": engine.position_evaluations[i],
1991 "depth": 15,
1992 "game_id": i
1993 }));
1994 }
1995 }
1996
1997 let file = File::create(json_path)?;
1999 let writer = BufWriter::new(file);
2000 serde_json::to_writer(writer, &data)?;
2001
2002 println!(
2003 "ā
Converted A100 data: {} positions ā {}",
2004 data.len(),
2005 json_path
2006 );
2007 Ok(())
2008 }
2009
2010 pub fn convert_to_zstd() -> Result<(), Box<dyn std::error::Error>> {
2013 use std::fs::File;
2014 use std::io::{BufReader, BufWriter};
2015
2016 if std::path::Path::new("training_data_a100.bin").exists() {
2018 Self::convert_a100_binary_to_json()?;
2019 }
2020
2021 let input_files = [
2022 ("training_data.json", "training_data.zst"),
2023 ("tactical_training_data.json", "tactical_training_data.zst"),
2024 ("training_data_a100.json", "training_data_a100.zst"),
2025 ("training_data.bin", "training_data.bin.zst"),
2026 (
2027 "tactical_training_data.bin",
2028 "tactical_training_data.bin.zst",
2029 ),
2030 ("training_data_a100.bin", "training_data_a100.bin.zst"),
2031 ];
2032
2033 for (input_file, output_file) in &input_files {
2034 let input_path = std::path::Path::new(input_file);
2035 if !input_path.exists() {
2036 continue;
2037 }
2038
2039 println!("š Converting {input_file} ā {output_file} (Zstd compression)");
2040
2041 let input_file = File::open(input_path)?;
2042 let output_file_handle = File::create(output_file)?;
2043 let writer = BufWriter::new(output_file_handle);
2044 let mut encoder = zstd::stream::Encoder::new(writer, 9)?; std::io::copy(&mut BufReader::new(input_file), &mut encoder)?;
2047 encoder.finish()?;
2048
2049 let input_size = input_path.metadata()?.len();
2050 let output_size = std::path::Path::new(output_file).metadata()?.len();
2051 let ratio = input_size as f64 / output_size as f64;
2052
2053 println!(
2054 "ā
Compressed: {} ā {} ({:.1}x size reduction)",
2055 Self::format_bytes(input_size),
2056 Self::format_bytes(output_size),
2057 ratio
2058 );
2059 }
2060
2061 Ok(())
2062 }
2063
2064 pub fn convert_to_mmap() -> Result<(), Box<dyn std::error::Error>> {
2067 use std::fs::File;
2068 use std::io::{BufReader, BufWriter};
2069
2070 if std::path::Path::new("training_data_a100.bin").exists() {
2072 Self::convert_a100_binary_to_json()?;
2073 }
2074
2075 let input_files = [
2076 ("training_data.json", "training_data.mmap"),
2077 ("tactical_training_data.json", "tactical_training_data.mmap"),
2078 ("training_data_a100.json", "training_data_a100.mmap"),
2079 ("training_data.msgpack", "training_data.mmap"),
2080 (
2081 "tactical_training_data.msgpack",
2082 "tactical_training_data.mmap",
2083 ),
2084 ("training_data_a100.msgpack", "training_data_a100.mmap"),
2085 ];
2086
2087 for (input_file, output_file) in &input_files {
2088 let input_path = std::path::Path::new(input_file);
2089 if !input_path.exists() {
2090 continue;
2091 }
2092
2093 println!("š Converting {input_file} ā {output_file} (Memory-mapped format)");
2094
2095 let data: Vec<(String, f32)> = if input_file.ends_with(".json") {
2097 let file = File::open(input_path)?;
2098 let reader = BufReader::new(file);
2099 let json_value: Value = serde_json::from_reader(reader)?;
2100
2101 match json_value {
2102 Value::Array(arr) if !arr.is_empty() => {
2104 if let Some(first) = arr.first() {
2105 if first.is_array() {
2106 arr.into_iter()
2108 .filter_map(|item| {
2109 if let Value::Array(tuple) = item {
2110 if tuple.len() >= 2 {
2111 let fen = tuple[0].as_str()?.to_string();
2112 let mut eval = tuple[1].as_f64()? as f32;
2113
2114 if eval.abs() > 15.0 {
2118 eval /= 100.0;
2119 }
2120
2121 Some((fen, eval))
2122 } else {
2123 None
2124 }
2125 } else {
2126 None
2127 }
2128 })
2129 .collect()
2130 } else if first.is_object() {
2131 arr.into_iter()
2133 .filter_map(|item| {
2134 if let Value::Object(obj) = item {
2135 let fen = obj.get("fen")?.as_str()?.to_string();
2136 let mut eval = obj.get("evaluation")?.as_f64()? as f32;
2137
2138 if eval.abs() > 15.0 {
2142 eval /= 100.0;
2143 }
2144
2145 Some((fen, eval))
2146 } else {
2147 None
2148 }
2149 })
2150 .collect()
2151 } else {
2152 return Err("Failed to process training data".into());
2153 }
2154 } else {
2155 Vec::new()
2156 }
2157 }
2158 _ => return Err("Processing...".to_string().into()),
2159 }
2160 } else if input_file.ends_with(".msgpack") {
2161 let file = File::open(input_path)?;
2162 let reader = BufReader::new(file);
2163 rmp_serde::from_read(reader)?
2164 } else {
2165 return Err("Unsupported input format for memory mapping".into());
2166 };
2167
2168 let output_file_handle = File::create(output_file)?;
2170 let mut writer = BufWriter::new(output_file_handle);
2171 rmp_serde::encode::write(&mut writer, &data)?;
2172
2173 let input_size = input_path.metadata()?.len();
2174 let output_size = std::path::Path::new(output_file).metadata()?.len();
2175
2176 println!(
2177 "ā
Memory-mapped file created: {} ā {} ({} positions)",
2178 Self::format_bytes(input_size),
2179 Self::format_bytes(output_size),
2180 data.len()
2181 );
2182 }
2183
2184 Ok(())
2185 }
2186
2187 pub fn convert_json_to_binary() -> Result<Vec<String>, Box<dyn std::error::Error>> {
2189 use indicatif::{ProgressBar, ProgressStyle};
2190
2191 let json_files = [
2192 "training_data.json",
2193 "tactical_training_data.json",
2194 "engine_training.json",
2195 "chess_training.json",
2196 ];
2197
2198 let existing_json_files: Vec<_> = json_files
2200 .iter()
2201 .filter(|&file_path| std::path::Path::new(file_path).exists())
2202 .collect();
2203
2204 if existing_json_files.is_empty() {
2205 println!("ā¹ļø No JSON training files found to convert");
2206 return Ok(Vec::new());
2207 }
2208
2209 println!(
2210 "š Converting {} JSON files to binary format...",
2211 existing_json_files.len()
2212 );
2213
2214 let pb = ProgressBar::new(existing_json_files.len() as u64);
2216 pb.set_style(
2217 ProgressStyle::default_bar()
2218 .template(
2219 "š¦ Converting [{elapsed_precise}] [{bar:40.yellow/blue}] {pos}/{len} {msg}",
2220 )?
2221 .progress_chars("āāā"),
2222 );
2223
2224 let mut converted_files = Vec::new();
2225
2226 for (i, json_file) in existing_json_files.iter().enumerate() {
2227 pb.set_position(i as u64);
2228 pb.set_message("Processing...".to_string());
2229
2230 let binary_file = std::path::Path::new(json_file).with_extension("bin");
2231
2232 let mut temp_engine = Self::new(1024);
2234 if temp_engine
2235 .load_training_data_incremental(json_file)
2236 .is_ok()
2237 {
2238 if temp_engine.save_training_data_binary(&binary_file).is_ok() {
2239 converted_files.push(binary_file.to_string_lossy().to_string());
2240 println!("ā
Converted {json_file} to binary format");
2241 } else {
2242 println!("Loading complete");
2243 }
2244 } else {
2245 println!("Loading complete");
2246 }
2247 }
2248
2249 pb.set_position(existing_json_files.len() as u64);
2250 pb.finish_with_message(format!("ā
Converted {} files", converted_files.len()));
2251
2252 if !converted_files.is_empty() {
2253 println!("š Binary conversion complete! Startup will be 5-15x faster next time.");
2254 println!("š Conversion summary:");
2255 for _conversion in &converted_files {
2256 println!("Loading complete");
2257 }
2258 }
2259
2260 Ok(converted_files)
2261 }
2262
2263 pub fn is_lsh_enabled(&self) -> bool {
2265 self.use_lsh
2266 }
2267
2268 pub fn lsh_stats(&self) -> Option<crate::lsh::LSHStats> {
2270 self.lsh_index.as_ref().map(|lsh| lsh.stats())
2271 }
2272
2273 pub fn enable_manifold_learning(&mut self, compression_ratio: f32) -> Result<(), String> {
2275 let input_dim = self.encoder.vector_size();
2276 let output_dim = ((input_dim as f32) / compression_ratio) as usize;
2277
2278 if output_dim == 0 {
2279 return Err("Compression ratio too high, output dimension would be 0".to_string());
2280 }
2281
2282 let mut learner = ManifoldLearner::new(input_dim, output_dim);
2283 learner.init_network()?;
2284
2285 self.manifold_learner = Some(learner);
2286 self.manifold_similarity_search = Some(SimilaritySearch::new(output_dim));
2287 self.use_manifold = false; Ok(())
2290 }
2291
2292 pub fn train_manifold_learning(&mut self, epochs: usize) -> Result<(), String> {
2294 if self.manifold_learner.is_none() {
2295 return Err(
2296 "Manifold learning not enabled. Call enable_manifold_learning first.".to_string(),
2297 );
2298 }
2299
2300 if self.similarity_search.size() == 0 {
2301 return Err("No positions in knowledge base to train on.".to_string());
2302 }
2303
2304 let rows = self.similarity_search.size();
2306 let cols = self.encoder.vector_size();
2307
2308 let training_matrix = Array2::from_shape_fn((rows, cols), |(row, col)| {
2309 if let Some((vector, _)) = self.similarity_search.get_position_ref(row) {
2310 vector[col]
2311 } else {
2312 0.0
2313 }
2314 });
2315
2316 if let Some(ref mut learner) = self.manifold_learner {
2318 learner.train(&training_matrix, epochs)?;
2319 let compression_ratio = learner.compression_ratio();
2320
2321 let _ = learner;
2323
2324 self.rebuild_manifold_indices()?;
2326 self.use_manifold = true;
2327
2328 println!(
2329 "Manifold learning training completed. Compression ratio: {compression_ratio:.1}x"
2330 );
2331 }
2332
2333 Ok(())
2334 }
2335
2336 fn rebuild_manifold_indices(&mut self) -> Result<(), String> {
2338 if let Some(ref learner) = self.manifold_learner {
2339 let output_dim = learner.output_dim();
2341 if let Some(ref mut search) = self.manifold_similarity_search {
2342 *search = SimilaritySearch::new(output_dim);
2343 }
2344 if let Some(ref mut lsh) = self.manifold_lsh_index {
2345 *lsh = LSH::new(output_dim, 8, 16); }
2347
2348 for (vector, eval) in self.similarity_search.iter_positions() {
2350 let compressed = learner.encode(vector);
2351
2352 if let Some(ref mut search) = self.manifold_similarity_search {
2353 search.add_position(compressed.clone(), eval);
2354 }
2355
2356 if let Some(ref mut lsh) = self.manifold_lsh_index {
2357 lsh.add_vector(compressed, eval);
2358 }
2359 }
2360 }
2361
2362 Ok(())
2363 }
2364
2365 pub fn enable_manifold_lsh(
2367 &mut self,
2368 num_tables: usize,
2369 hash_size: usize,
2370 ) -> Result<(), String> {
2371 if self.manifold_learner.is_none() {
2372 return Err("Manifold learning not enabled".to_string());
2373 }
2374
2375 let output_dim = self.manifold_learner.as_ref().unwrap().output_dim();
2376 self.manifold_lsh_index = Some(LSH::new(output_dim, num_tables, hash_size));
2377
2378 if self.use_manifold {
2380 self.rebuild_manifold_indices()?;
2381 }
2382
2383 Ok(())
2384 }
2385
2386 pub fn is_manifold_enabled(&self) -> bool {
2388 self.use_manifold && self.manifold_learner.is_some()
2389 }
2390
2391 pub fn manifold_compression_ratio(&self) -> Option<f32> {
2393 self.manifold_learner
2394 .as_ref()
2395 .map(|l| l.compression_ratio())
2396 }
2397
2398 pub fn load_manifold_models(&mut self) -> Result<(), Box<dyn std::error::Error>> {
2401 if let Some(ref db) = self.database {
2402 match crate::manifold_learner::ManifoldLearner::load_from_database(db)? {
2403 Some(learner) => {
2404 let compression_ratio = learner.compression_ratio();
2405 println!(
2406 "š§ Loaded pre-trained manifold learner (compression: {compression_ratio:.1}x)"
2407 );
2408
2409 self.manifold_learner = Some(learner);
2411 self.use_manifold = true;
2412
2413 self.rebuild_manifold_indices()?;
2415
2416 println!("ā
Manifold learning enabled with compressed vectors");
2417 Ok(())
2418 }
2419 None => Err("No pre-trained manifold models found in database".into()),
2420 }
2421 } else {
2422 Err("Database not initialized - cannot load manifold models".into())
2423 }
2424 }
2425
2426 pub fn enable_opening_book(&mut self) {
2428 self.opening_book = Some(OpeningBook::with_standard_openings());
2429 }
2430
2431 pub fn set_opening_book(&mut self, book: OpeningBook) {
2433 self.opening_book = Some(book);
2434 }
2435
2436 pub fn is_opening_position(&self, board: &Board) -> bool {
2438 self.opening_book
2439 .as_ref()
2440 .map(|book| book.contains(board))
2441 .unwrap_or(false)
2442 }
2443
2444 pub fn get_opening_entry(&self, board: &Board) -> Option<&OpeningEntry> {
2446 self.opening_book.as_ref()?.lookup(board)
2447 }
2448
2449 pub fn opening_book_stats(&self) -> Option<OpeningBookStats> {
2451 self.opening_book.as_ref().map(|book| book.get_statistics())
2452 }
2453
2454 pub fn add_position_with_move(
2456 &mut self,
2457 board: &Board,
2458 evaluation: f32,
2459 chess_move: Option<ChessMove>,
2460 move_outcome: Option<f32>,
2461 ) {
2462 let position_index = self.knowledge_base_size();
2463
2464 self.add_position(board, evaluation);
2466
2467 if let (Some(mov), Some(outcome)) = (chess_move, move_outcome) {
2469 self.position_moves
2470 .entry(position_index)
2471 .or_default()
2472 .push((mov, outcome));
2473 }
2474 }
2475
2476 pub fn recommend_moves_with_tactical_search(
2478 &mut self,
2479 board: &Board,
2480 num_recommendations: usize,
2481 ) -> Vec<MoveRecommendation> {
2482 use chess::MoveGen;
2484 let legal_moves: Vec<ChessMove> = MoveGen::new_legal(board).collect();
2485
2486 if legal_moves.is_empty() {
2487 return Vec::new();
2488 }
2489
2490 let mut move_evaluations = Vec::new();
2491
2492 for chess_move in legal_moves.iter().take(20) {
2493 let temp_board = board.make_move_new(*chess_move);
2495
2496 let evaluation = if let Some(ref mut tactical_search) = self.tactical_search {
2498 let result = tactical_search.search(&temp_board);
2499 result.evaluation
2500 } else {
2501 self.evaluate_position(&temp_board).unwrap_or(0.0)
2503 };
2504
2505 let normalized_eval = if board.side_to_move() == chess::Color::White {
2506 evaluation
2507 } else {
2508 -evaluation
2509 };
2510
2511 move_evaluations.push((*chess_move, normalized_eval));
2512 }
2513
2514 move_evaluations.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
2516
2517 let mut recommendations = Vec::new();
2519 for (i, (chess_move, evaluation)) in move_evaluations
2520 .iter()
2521 .enumerate()
2522 .take(num_recommendations)
2523 {
2524 let confidence = if i == 0 { 0.8 } else { 0.6 - (i as f32 * 0.1) }; recommendations.push(MoveRecommendation {
2526 chess_move: *chess_move,
2527 confidence: confidence.max(0.3),
2528 from_similar_position_count: 0,
2529 average_outcome: *evaluation,
2530 });
2531 }
2532
2533 recommendations
2534 }
2535
2536 pub fn recommend_moves(
2538 &mut self,
2539 board: &Board,
2540 num_recommendations: usize,
2541 ) -> Vec<MoveRecommendation> {
2542 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
2544 let proactive_moves = strategic_evaluator.generate_proactive_moves(board);
2545
2546 if !proactive_moves.is_empty() {
2547 let mut strategic_recommendations = Vec::new();
2548
2549 for (chess_move, strategic_value) in
2550 proactive_moves.iter().take(num_recommendations)
2551 {
2552 strategic_recommendations.push(MoveRecommendation {
2553 chess_move: *chess_move,
2554 confidence: (strategic_value / 80.0).clamp(0.3, 0.95), from_similar_position_count: 0, average_outcome: *strategic_value / 100.0, });
2558 }
2559
2560 if strategic_recommendations.len() >= num_recommendations {
2562 strategic_recommendations.truncate(num_recommendations);
2563 return strategic_recommendations;
2564 }
2565
2566 } else {
2569 return self.recommend_moves_with_tactical_search(board, num_recommendations);
2572 }
2573 }
2574
2575 if let Some(entry) = self.get_opening_entry(board) {
2589 let mut recommendations = Vec::new();
2590
2591 for (chess_move, strength) in &entry.best_moves {
2592 recommendations.push(MoveRecommendation {
2593 chess_move: *chess_move,
2594 confidence: strength * 0.9, from_similar_position_count: 1,
2596 average_outcome: entry.evaluation,
2597 });
2598 }
2599
2600 recommendations.sort_by(|a, b| {
2602 b.confidence
2603 .partial_cmp(&a.confidence)
2604 .unwrap_or(std::cmp::Ordering::Equal)
2605 });
2606 recommendations.truncate(num_recommendations);
2607 return recommendations;
2608 }
2609
2610 let similar_positions = self.find_similar_positions_with_indices(board, 20);
2612
2613 let mut move_data: HashMap<ChessMove, Vec<(f32, f32)>> = HashMap::new(); use chess::MoveGen;
2618 let legal_moves: Vec<ChessMove> = match std::panic::catch_unwind(|| {
2619 MoveGen::new_legal(board).collect::<Vec<ChessMove>>()
2620 }) {
2621 Ok(moves) => moves,
2622 Err(_) => {
2623 return Vec::new();
2625 }
2626 };
2627
2628 for (position_index, _eval, similarity) in similar_positions {
2630 if let Some(moves) = self.position_moves.get(&position_index) {
2631 for &(chess_move, outcome) in moves {
2632 if legal_moves.contains(&chess_move) {
2634 move_data
2635 .entry(chess_move)
2636 .or_default()
2637 .push((similarity, outcome));
2638 }
2639 }
2640 }
2641 }
2642
2643 if self.tactical_search.is_some() {
2645 if let Some(ref mut tactical_search) = self.tactical_search {
2646 let tactical_result =
2648 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
2649 if strategic_evaluator.should_play_aggressively(board) {
2651 tactical_search.search(board)
2653 } else {
2654 tactical_search.search(board)
2656 }
2657 } else {
2658 tactical_search.search(board)
2660 };
2661
2662 if let Some(best_move) = tactical_result.best_move {
2664 let mut temp_board = *board;
2666 temp_board = temp_board.make_move_new(best_move);
2667 let move_evaluation = tactical_search.search(&temp_board).evaluation;
2668
2669 let confidence = if let Some(ref strategic_evaluator) = self.strategic_evaluator
2671 {
2672 let strategic_eval = strategic_evaluator.evaluate_strategic(board);
2673 if strategic_eval.attacking_moves.contains(&best_move) {
2675 0.98 } else if strategic_eval.positional_moves.contains(&best_move) {
2677 0.95 } else {
2679 0.90 }
2681 } else {
2682 0.95 };
2684
2685 move_data.insert(best_move, vec![(confidence, move_evaluation)]);
2686 }
2687
2688 let mut ordered_moves = legal_moves.clone();
2691
2692 ordered_moves.sort_by(|a, b| {
2694 let a_is_capture = board.piece_on(a.get_dest()).is_some();
2695 let b_is_capture = board.piece_on(b.get_dest()).is_some();
2696
2697 match (a_is_capture, b_is_capture) {
2698 (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => {
2701 let a_centrality = move_centrality(a);
2703 let b_centrality = move_centrality(b);
2704 b_centrality
2705 .partial_cmp(&a_centrality)
2706 .unwrap_or(std::cmp::Ordering::Equal)
2707 }
2708 }
2709 });
2710
2711 for chess_move in ordered_moves.into_iter() {
2714 move_data.entry(chess_move).or_insert_with(|| {
2715 let mut temp_board = *board;
2717 temp_board = temp_board.make_move_new(chess_move);
2718 let move_evaluation = tactical_search.search(&temp_board).evaluation;
2719
2720 let confidence =
2722 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
2723 let strategic_eval = strategic_evaluator.evaluate_strategic(board);
2724 if strategic_eval.attacking_moves.contains(&chess_move) {
2726 0.92 } else if strategic_eval.positional_moves.contains(&chess_move) {
2728 0.88 } else {
2730 0.85 }
2732 } else {
2733 0.90 };
2735
2736 vec![(confidence, move_evaluation)]
2737 });
2738 }
2739 } else {
2740 let mut ordered_moves = legal_moves.clone();
2743
2744 ordered_moves.sort_by(|a, b| {
2746 let a_is_capture = board.piece_on(a.get_dest()).is_some();
2747 let b_is_capture = board.piece_on(b.get_dest()).is_some();
2748
2749 match (a_is_capture, b_is_capture) {
2750 (true, false) => std::cmp::Ordering::Less,
2751 (false, true) => std::cmp::Ordering::Greater,
2752 _ => {
2753 let a_centrality = move_centrality(a);
2754 let b_centrality = move_centrality(b);
2755 b_centrality
2756 .partial_cmp(&a_centrality)
2757 .unwrap_or(std::cmp::Ordering::Equal)
2758 }
2759 }
2760 });
2761
2762 for chess_move in ordered_moves.into_iter().take(num_recommendations) {
2763 let mut basic_eval = 0.0;
2765
2766 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
2768 basic_eval += match captured_piece {
2769 chess::Piece::Pawn => 1.0,
2770 chess::Piece::Knight | chess::Piece::Bishop => 3.0,
2771 chess::Piece::Rook => 5.0,
2772 chess::Piece::Queen => 9.0,
2773 chess::Piece::King => 100.0, };
2775 }
2776
2777 move_data.insert(chess_move, vec![(0.3, basic_eval)]); }
2779 }
2780 }
2781
2782 let mut recommendations = Vec::new();
2784
2785 for (chess_move, outcomes) in move_data {
2786 if outcomes.is_empty() {
2787 continue;
2788 }
2789
2790 let mut weighted_sum = 0.0;
2792 let mut weight_sum = 0.0;
2793
2794 for &(similarity, outcome) in &outcomes {
2795 weighted_sum += similarity * outcome;
2796 weight_sum += similarity;
2797 }
2798
2799 let average_outcome = if weight_sum > 0.0 {
2800 weighted_sum / weight_sum
2801 } else {
2802 0.0
2803 };
2804
2805 let avg_similarity =
2807 outcomes.iter().map(|(s, _)| s).sum::<f32>() / outcomes.len() as f32;
2808 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 {
2812 chess_move,
2813 confidence: confidence.min(1.0), from_similar_position_count: outcomes.len(),
2815 average_outcome,
2816 });
2817 }
2818
2819 recommendations.sort_by(|a, b| {
2822 match board.side_to_move() {
2823 chess::Color::White => {
2824 b.average_outcome
2826 .partial_cmp(&a.average_outcome)
2827 .unwrap_or(std::cmp::Ordering::Equal)
2828 }
2829 chess::Color::Black => {
2830 a.average_outcome
2832 .partial_cmp(&b.average_outcome)
2833 .unwrap_or(std::cmp::Ordering::Equal)
2834 }
2835 }
2836 });
2837
2838 recommendations = self.apply_hanging_piece_safety_checks(board, recommendations);
2840
2841 recommendations.truncate(num_recommendations);
2843 recommendations
2844 }
2845
2846 fn apply_hanging_piece_safety_checks(
2849 &mut self,
2850 board: &Board,
2851 mut recommendations: Vec<MoveRecommendation>,
2852 ) -> Vec<MoveRecommendation> {
2853 use chess::{MoveGen, Piece};
2854
2855 for recommendation in &mut recommendations {
2856 let mut safety_penalty = 0.0;
2857
2858 let mut temp_board = *board;
2860 temp_board = temp_board.make_move_new(recommendation.chess_move);
2861
2862 let our_color = board.side_to_move();
2864 let opponent_color = !our_color;
2865
2866 let opponent_moves: Vec<chess::ChessMove> = MoveGen::new_legal(&temp_board).collect();
2868
2869 for square in chess::ALL_SQUARES {
2871 if let Some(piece) = temp_board.piece_on(square) {
2872 if temp_board.color_on(square) == Some(our_color) {
2873 let piece_value = match piece {
2875 Piece::Pawn => 1.0,
2876 Piece::Knight | Piece::Bishop => 3.0,
2877 Piece::Rook => 5.0,
2878 Piece::Queen => 9.0,
2879 Piece::King => 0.0, };
2881
2882 let can_be_captured =
2884 opponent_moves.iter().any(|&mv| mv.get_dest() == square);
2885
2886 if can_be_captured {
2887 let is_defended =
2889 self.is_piece_defended(&temp_board, square, our_color);
2890
2891 if !is_defended {
2892 safety_penalty += piece_value * 2.0; } else {
2895 safety_penalty += piece_value * 0.1; }
2898 }
2899 }
2900 }
2901 }
2902
2903 let original_threats = self.find_immediate_threats(board, opponent_color);
2905 let resolved_threats =
2906 self.count_resolved_threats(board, &temp_board, &original_threats);
2907
2908 if !original_threats.is_empty() && resolved_threats == 0 {
2910 safety_penalty += 2.0; }
2912
2913 let penalty_factor = 1.0 - (safety_penalty * 0.2_f32).min(0.8);
2915 recommendation.confidence *= penalty_factor;
2916 recommendation.confidence = recommendation.confidence.max(0.1);
2917
2918 recommendation.average_outcome -= safety_penalty;
2920 }
2921
2922 recommendations.sort_by(|a, b| {
2924 let confidence_cmp = b
2926 .confidence
2927 .partial_cmp(&a.confidence)
2928 .unwrap_or(std::cmp::Ordering::Equal);
2929 if confidence_cmp != std::cmp::Ordering::Equal {
2930 return confidence_cmp;
2931 }
2932
2933 match board.side_to_move() {
2935 chess::Color::White => b
2936 .average_outcome
2937 .partial_cmp(&a.average_outcome)
2938 .unwrap_or(std::cmp::Ordering::Equal),
2939 chess::Color::Black => a
2940 .average_outcome
2941 .partial_cmp(&b.average_outcome)
2942 .unwrap_or(std::cmp::Ordering::Equal),
2943 }
2944 });
2945
2946 recommendations
2947 }
2948
2949 fn is_piece_defended(
2951 &self,
2952 board: &Board,
2953 square: chess::Square,
2954 our_color: chess::Color,
2955 ) -> bool {
2956 use chess::ALL_SQUARES;
2957
2958 for source_square in ALL_SQUARES {
2960 if let Some(piece) = board.piece_on(source_square) {
2961 if board.color_on(source_square) == Some(our_color) {
2962 if self.can_piece_attack(board, piece, source_square, square) {
2964 return true;
2965 }
2966 }
2967 }
2968 }
2969
2970 false
2971 }
2972
2973 fn can_piece_attack(
2975 &self,
2976 board: &Board,
2977 piece: chess::Piece,
2978 from: chess::Square,
2979 to: chess::Square,
2980 ) -> bool {
2981 use chess::Piece;
2982
2983 match piece {
2986 Piece::Pawn => {
2987 let from_file = from.get_file().to_index();
2989 let from_rank = from.get_rank().to_index();
2990 let to_file = to.get_file().to_index();
2991 let to_rank = to.get_rank().to_index();
2992
2993 let file_diff = (to_file as i32 - from_file as i32).abs();
2994 let rank_diff = to_rank as i32 - from_rank as i32;
2995
2996 file_diff == 1 && {
2998 match board.color_on(from).unwrap() {
2999 chess::Color::White => rank_diff == 1,
3000 chess::Color::Black => rank_diff == -1,
3001 }
3002 }
3003 }
3004 Piece::Knight => {
3005 let from_file = from.get_file().to_index() as i32;
3007 let from_rank = from.get_rank().to_index() as i32;
3008 let to_file = to.get_file().to_index() as i32;
3009 let to_rank = to.get_rank().to_index() as i32;
3010
3011 let file_diff = (to_file - from_file).abs();
3012 let rank_diff = (to_rank - from_rank).abs();
3013
3014 (file_diff == 2 && rank_diff == 1) || (file_diff == 1 && rank_diff == 2)
3015 }
3016 Piece::Bishop => {
3017 self.is_diagonal_clear(board, from, to)
3019 }
3020 Piece::Rook => {
3021 self.is_straight_clear(board, from, to)
3023 }
3024 Piece::Queen => {
3025 self.is_diagonal_clear(board, from, to) || self.is_straight_clear(board, from, to)
3027 }
3028 Piece::King => {
3029 let from_file = from.get_file().to_index() as i32;
3031 let from_rank = from.get_rank().to_index() as i32;
3032 let to_file = to.get_file().to_index() as i32;
3033 let to_rank = to.get_rank().to_index() as i32;
3034
3035 let file_diff = (to_file - from_file).abs();
3036 let rank_diff = (to_rank - from_rank).abs();
3037
3038 file_diff <= 1 && rank_diff <= 1 && (file_diff != 0 || rank_diff != 0)
3039 }
3040 }
3041 }
3042
3043 fn is_diagonal_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
3045 let from_file = from.get_file().to_index() as i32;
3046 let from_rank = from.get_rank().to_index() as i32;
3047 let to_file = to.get_file().to_index() as i32;
3048 let to_rank = to.get_rank().to_index() as i32;
3049
3050 let file_diff = to_file - from_file;
3051 let rank_diff = to_rank - from_rank;
3052
3053 if file_diff.abs() != rank_diff.abs() || file_diff == 0 {
3055 return false;
3056 }
3057
3058 let file_step = if file_diff > 0 { 1 } else { -1 };
3059 let rank_step = if rank_diff > 0 { 1 } else { -1 };
3060
3061 let steps = file_diff.abs();
3062
3063 for i in 1..steps {
3065 let check_file = from_file + i * file_step;
3066 let check_rank = from_rank + i * rank_step;
3067
3068 let check_square = chess::Square::make_square(
3069 chess::Rank::from_index(check_rank as usize),
3070 chess::File::from_index(check_file as usize),
3071 );
3072 if board.piece_on(check_square).is_some() {
3073 return false; }
3075 }
3076
3077 true
3078 }
3079
3080 fn is_straight_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
3082 let from_file = from.get_file().to_index() as i32;
3083 let from_rank = from.get_rank().to_index() as i32;
3084 let to_file = to.get_file().to_index() as i32;
3085 let to_rank = to.get_rank().to_index() as i32;
3086
3087 if from_file != to_file && from_rank != to_rank {
3089 return false;
3090 }
3091
3092 if from_file == to_file {
3093 let start_rank = from_rank.min(to_rank);
3095 let end_rank = from_rank.max(to_rank);
3096
3097 for rank in (start_rank + 1)..end_rank {
3098 let check_square = chess::Square::make_square(
3099 chess::Rank::from_index(rank as usize),
3100 chess::File::from_index(from_file as usize),
3101 );
3102 if board.piece_on(check_square).is_some() {
3103 return false; }
3105 }
3106 } else {
3107 let start_file = from_file.min(to_file);
3109 let end_file = from_file.max(to_file);
3110
3111 for file in (start_file + 1)..end_file {
3112 let check_square = chess::Square::make_square(
3113 chess::Rank::from_index(from_rank as usize),
3114 chess::File::from_index(file as usize),
3115 );
3116 if board.piece_on(check_square).is_some() {
3117 return false; }
3119 }
3120 }
3121
3122 true
3123 }
3124
3125 fn find_immediate_threats(
3127 &self,
3128 board: &Board,
3129 opponent_color: chess::Color,
3130 ) -> Vec<(chess::Square, f32)> {
3131 use chess::MoveGen;
3132
3133 let mut threats = Vec::new();
3134
3135 let opponent_moves: Vec<chess::ChessMove> = MoveGen::new_legal(board).collect();
3137
3138 for mv in opponent_moves {
3139 let target_square = mv.get_dest();
3140 if let Some(piece) = board.piece_on(target_square) {
3141 if board.color_on(target_square) == Some(!opponent_color) {
3142 let piece_value = match piece {
3144 chess::Piece::Pawn => 1.0,
3145 chess::Piece::Knight | chess::Piece::Bishop => 3.0,
3146 chess::Piece::Rook => 5.0,
3147 chess::Piece::Queen => 9.0,
3148 chess::Piece::King => 100.0,
3149 };
3150 threats.push((target_square, piece_value));
3151 }
3152 }
3153 }
3154
3155 threats
3156 }
3157
3158 fn count_resolved_threats(
3160 &self,
3161 original_board: &Board,
3162 new_board: &Board,
3163 original_threats: &[(chess::Square, f32)],
3164 ) -> usize {
3165 let mut resolved = 0;
3166
3167 for &(threatened_square, _value) in original_threats {
3168 let piece_still_there =
3170 new_board.piece_on(threatened_square) == original_board.piece_on(threatened_square);
3171
3172 if !piece_still_there {
3173 resolved += 1;
3175 } else {
3176 let still_threatened = self
3178 .find_immediate_threats(new_board, new_board.side_to_move())
3179 .iter()
3180 .any(|&(square, _)| square == threatened_square);
3181
3182 if !still_threatened {
3183 resolved += 1;
3184 }
3185 }
3186 }
3187
3188 resolved
3189 }
3190
3191 pub fn recommend_legal_moves(
3193 &mut self,
3194 board: &Board,
3195 num_recommendations: usize,
3196 ) -> Vec<MoveRecommendation> {
3197 use chess::MoveGen;
3198
3199 let legal_moves: std::collections::HashSet<ChessMove> = MoveGen::new_legal(board).collect();
3201
3202 let all_recommendations = self.recommend_moves(board, num_recommendations * 2); all_recommendations
3206 .into_iter()
3207 .filter(|rec| legal_moves.contains(&rec.chess_move))
3208 .take(num_recommendations)
3209 .collect()
3210 }
3211
3212 pub fn enable_persistence<P: AsRef<Path>>(
3214 &mut self,
3215 db_path: P,
3216 ) -> Result<(), Box<dyn std::error::Error>> {
3217 let database = Database::new(db_path)?;
3218 self.database = Some(database);
3219 println!("Persistence enabled");
3220 Ok(())
3221 }
3222
3223 pub fn save_to_database(&self) -> Result<(), Box<dyn std::error::Error>> {
3225 let db = self
3226 .database
3227 .as_ref()
3228 .ok_or("Database not enabled. Call enable_persistence() first.")?;
3229
3230 println!("š¾ Saving engine state to database (batch mode)...");
3231
3232 let current_time = std::time::SystemTime::now()
3234 .duration_since(std::time::UNIX_EPOCH)?
3235 .as_secs() as i64;
3236
3237 let mut position_data_batch = Vec::with_capacity(self.position_boards.len());
3238
3239 for (i, board) in self.position_boards.iter().enumerate() {
3240 if i < self.position_vectors.len() && i < self.position_evaluations.len() {
3241 let vector = self.position_vectors[i].as_slice().unwrap();
3242 let position_data = PositionData {
3243 fen: board.to_string(),
3244 vector: vector.iter().map(|&x| x as f64).collect(),
3245 evaluation: Some(self.position_evaluations[i] as f64),
3246 compressed_vector: None, created_at: current_time,
3248 };
3249 position_data_batch.push(position_data);
3250 }
3251 }
3252
3253 if !position_data_batch.is_empty() {
3255 let saved_count = db.save_positions_batch(&position_data_batch)?;
3256 println!("š Batch saved {saved_count} positions");
3257 }
3258
3259 if let Some(ref lsh) = self.lsh_index {
3261 lsh.save_to_database(db)?;
3262 }
3263
3264 if let Some(ref learner) = self.manifold_learner {
3266 if learner.is_trained() {
3267 learner.save_to_database(db)?;
3268 }
3269 }
3270
3271 println!("ā
Engine state saved successfully (batch optimized)");
3272 Ok(())
3273 }
3274
3275 pub fn load_from_database(&mut self) -> Result<(), Box<dyn std::error::Error>> {
3277 let db = self
3278 .database
3279 .as_ref()
3280 .ok_or("Database not enabled. Call enable_persistence() first.")?;
3281
3282 println!("Loading engine state from database...");
3283
3284 let positions = db.load_all_positions()?;
3286 for position_data in positions {
3287 if let Ok(board) = Board::from_str(&position_data.fen) {
3288 let vector: Vec<f32> = position_data.vector.iter().map(|&x| x as f32).collect();
3289 let vector_array = Array1::from(vector);
3290 let mut evaluation = position_data.evaluation.unwrap_or(0.0) as f32;
3291
3292 if evaluation.abs() > 15.0 {
3296 evaluation /= 100.0;
3297 }
3298
3299 self.similarity_search
3301 .add_position(vector_array.clone(), evaluation);
3302
3303 self.position_vectors.push(vector_array);
3305 self.position_boards.push(board);
3306 self.position_evaluations.push(evaluation);
3307 }
3308 }
3309
3310 if self.use_lsh {
3312 let positions_for_lsh: Vec<(Array1<f32>, f32)> = self
3313 .position_vectors
3314 .iter()
3315 .zip(self.position_evaluations.iter())
3316 .map(|(v, &e)| (v.clone(), e))
3317 .collect();
3318
3319 match LSH::load_from_database(db, &positions_for_lsh)? {
3320 Some(lsh) => {
3321 self.lsh_index = Some(lsh);
3322 println!("Loaded LSH configuration from database");
3323 }
3324 None => {
3325 println!("No LSH configuration found in database");
3326 }
3327 }
3328 }
3329
3330 match ManifoldLearner::load_from_database(db)? {
3332 Some(learner) => {
3333 self.manifold_learner = Some(learner);
3334 if self.use_manifold {
3335 self.rebuild_manifold_indices()?;
3336 }
3337 println!("Loaded manifold learner from database");
3338 }
3339 None => {
3340 println!("No manifold learner found in database");
3341 }
3342 }
3343
3344 println!(
3345 "Engine state loaded successfully ({} positions)",
3346 self.knowledge_base_size()
3347 );
3348 Ok(())
3349 }
3350
3351 pub fn new_with_persistence<P: AsRef<Path>>(
3353 vector_size: usize,
3354 db_path: P,
3355 ) -> Result<Self, Box<dyn std::error::Error>> {
3356 let mut engine = Self::new(vector_size);
3357 engine.enable_persistence(db_path)?;
3358
3359 match engine.load_from_database() {
3361 Ok(_) => {
3362 println!("Loaded existing engine from database");
3363 }
3364 Err(e) => {
3365 println!("Starting fresh engine (load failed: {e})");
3366 }
3367 }
3368
3369 Ok(engine)
3370 }
3371
3372 pub fn auto_save(&self) -> Result<(), Box<dyn std::error::Error>> {
3374 if self.database.is_some() {
3375 self.save_to_database()?;
3376 }
3377 Ok(())
3378 }
3379
3380 pub fn is_persistence_enabled(&self) -> bool {
3382 self.database.is_some()
3383 }
3384
3385 pub fn database_position_count(&self) -> Result<i64, Box<dyn std::error::Error>> {
3387 let db = self.database.as_ref().ok_or("Database not enabled")?;
3388 Ok(db.get_position_count()?)
3389 }
3390
3391 pub fn enable_tactical_search(&mut self, config: TacticalConfig) {
3393 self.tactical_search = Some(TacticalSearch::new(config));
3394 }
3395
3396 pub fn enable_tactical_search_default(&mut self) {
3398 self.tactical_search = Some(TacticalSearch::new_default());
3399 }
3400
3401 pub fn configure_hybrid_evaluation(&mut self, config: HybridConfig) {
3403 self.hybrid_config = config;
3404 }
3405
3406 pub fn is_tactical_search_enabled(&self) -> bool {
3408 self.tactical_search.is_some()
3409 }
3410
3411 pub fn enable_parallel_search(&mut self, num_threads: usize) {
3413 if let Some(ref mut tactical_search) = self.tactical_search {
3414 tactical_search.config.enable_parallel_search = true;
3415 tactical_search.config.num_threads = num_threads;
3416 println!("š§µ Parallel tactical search enabled with {num_threads} threads");
3417 }
3418 }
3419
3420 pub fn is_parallel_search_enabled(&self) -> bool {
3422 self.tactical_search
3423 .as_ref()
3424 .map(|ts| ts.config.enable_parallel_search)
3425 .unwrap_or(false)
3426 }
3427
3428 pub fn enable_strategic_evaluation(&mut self, config: StrategicConfig) {
3435 self.strategic_evaluator = Some(StrategicEvaluator::new(config));
3436 println!("šÆ Strategic evaluation enabled - engine will play proactively");
3437 }
3438
3439 pub fn enable_strategic_evaluation_default(&mut self) {
3441 self.enable_strategic_evaluation(StrategicConfig::default());
3442 }
3443
3444 pub fn enable_strategic_evaluation_aggressive(&mut self) {
3446 self.enable_strategic_evaluation(StrategicConfig::aggressive());
3447 println!("āļø Aggressive strategic evaluation enabled - maximum initiative focus");
3448 }
3449
3450 pub fn enable_strategic_evaluation_positional(&mut self) {
3452 self.enable_strategic_evaluation(StrategicConfig::positional());
3453 println!("š Positional strategic evaluation enabled - long-term planning focus");
3454 }
3455
3456 pub fn is_strategic_evaluation_enabled(&self) -> bool {
3458 self.strategic_evaluator.is_some()
3459 }
3460
3461 pub fn get_strategic_evaluation(&self, board: &Board) -> Option<StrategicEvaluation> {
3463 self.strategic_evaluator
3464 .as_ref()
3465 .map(|evaluator| evaluator.evaluate_strategic(board))
3466 }
3467
3468 pub fn generate_proactive_moves(&self, board: &Board) -> Vec<(ChessMove, f32)> {
3471 if let Some(ref evaluator) = self.strategic_evaluator {
3472 evaluator.generate_proactive_moves(board)
3473 } else {
3474 Vec::new()
3476 }
3477 }
3478
3479 pub fn should_play_aggressively(&self, board: &Board) -> bool {
3481 if let Some(ref evaluator) = self.strategic_evaluator {
3482 evaluator.should_play_aggressively(board)
3483 } else {
3484 false }
3486 }
3487
3488 pub fn enable_nnue(&mut self) -> Result<(), Box<dyn std::error::Error>> {
3510 self.enable_nnue_with_auto_load(true)
3511 }
3512
3513 pub fn enable_nnue_with_auto_load(
3515 &mut self,
3516 auto_load: bool,
3517 ) -> Result<(), Box<dyn std::error::Error>> {
3518 let config = NNUEConfig::default();
3519 let mut nnue = NNUE::new(config)?;
3520
3521 if auto_load {
3523 if let Err(e) = self.try_load_default_nnue_model(&mut nnue) {
3524 println!("š Default NNUE model not found, using fresh model: {}", e);
3525 println!(
3526 " š” Create one with: cargo run --bin train_nnue -- --output default_hybrid"
3527 );
3528 } else {
3529 println!("ā
Auto-loaded default NNUE model (default_hybrid.config)");
3530
3531 if !nnue.are_weights_loaded() {
3533 println!("ā ļø Weights not properly applied, will use quick training fallback");
3534 } else {
3535 println!("ā
Weights successfully applied to feature transformer");
3536 }
3537 }
3538 }
3539
3540 self.nnue = Some(nnue);
3541 Ok(())
3542 }
3543
3544 fn try_load_default_nnue_model(
3546 &self,
3547 nnue: &mut NNUE,
3548 ) -> Result<(), Box<dyn std::error::Error>> {
3549 let default_paths = [
3551 "default_hybrid", "production_hybrid", "hybrid_production_nnue", "chess_nnue_advanced", "trained_nnue_model", ];
3557
3558 for path in &default_paths {
3559 let config_path = format!("{}.config", path);
3560 if std::path::Path::new(&config_path).exists() {
3561 nnue.load_model(path)?;
3562 return Ok(());
3563 }
3564 }
3565
3566 Err("No default NNUE model found in standard locations".into())
3567 }
3568
3569 pub fn enable_nnue_with_config(
3571 &mut self,
3572 config: NNUEConfig,
3573 ) -> Result<(), Box<dyn std::error::Error>> {
3574 self.nnue = Some(NNUE::new(config)?);
3575 Ok(())
3576 }
3577
3578 pub fn enable_nnue_with_model(
3580 &mut self,
3581 model_path: &str,
3582 ) -> Result<(), Box<dyn std::error::Error>> {
3583 let config = NNUEConfig::default();
3584 let mut nnue = NNUE::new(config)?;
3585 nnue.load_model(model_path)?;
3586 self.nnue = Some(nnue);
3587 Ok(())
3588 }
3589
3590 pub fn quick_fix_nnue_if_needed(&mut self) -> Result<(), Box<dyn std::error::Error>> {
3592 if let Some(ref mut nnue) = self.nnue {
3593 if !nnue.are_weights_loaded() {
3594 let training_positions = vec![(chess::Board::default(), 0.0)];
3596
3597 let mut positions = training_positions;
3599 if let Ok(board) = chess::Board::from_str(
3600 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
3601 ) {
3602 positions.push((board, 0.25));
3603 }
3604 if let Ok(board) = chess::Board::from_str("8/8/8/8/8/8/1K6/k6Q w - - 0 1") {
3605 positions.push((board, 9.0));
3606 }
3607
3608 nnue.quick_fix_training(&positions)?;
3609 }
3610 }
3611 Ok(())
3612 }
3613
3614 pub fn configure_nnue(&mut self, config: NNUEConfig) -> Result<(), Box<dyn std::error::Error>> {
3616 if self.nnue.is_some() {
3617 self.nnue = Some(NNUE::new(config)?);
3618 Ok(())
3619 } else {
3620 Err("NNUE must be enabled first before configuring".into())
3621 }
3622 }
3623
3624 pub fn is_nnue_enabled(&self) -> bool {
3626 self.nnue.is_some()
3627 }
3628
3629 pub fn train_nnue(
3631 &mut self,
3632 positions: &[(Board, f32)],
3633 ) -> Result<f32, Box<dyn std::error::Error>> {
3634 if let Some(ref mut nnue) = self.nnue {
3635 let loss = nnue.train_batch(positions)?;
3636 Ok(loss)
3637 } else {
3638 Err("NNUE must be enabled before training".into())
3639 }
3640 }
3641
3642 pub fn hybrid_config(&self) -> &HybridConfig {
3644 &self.hybrid_config
3645 }
3646
3647 pub fn is_opening_book_enabled(&self) -> bool {
3649 self.opening_book.is_some()
3650 }
3651
3652 pub fn self_play_training(
3654 &mut self,
3655 config: training::SelfPlayConfig,
3656 ) -> Result<usize, Box<dyn std::error::Error>> {
3657 let mut trainer = training::SelfPlayTrainer::new(config);
3658 let new_data = trainer.generate_training_data(self);
3659
3660 let positions_added = new_data.data.len();
3661
3662 for data in &new_data.data {
3664 self.add_position(&data.board, data.evaluation);
3665 }
3666
3667 if self.database.is_some() {
3669 match self.save_to_database() {
3670 Ok(_) => println!("š¾ Saved {positions_added} positions to database"),
3671 Err(_e) => println!("Loading complete"),
3672 }
3673 }
3674
3675 println!("š§ Self-play training complete: {positions_added} new positions learned");
3676 Ok(positions_added)
3677 }
3678
3679 pub fn continuous_self_play(
3681 &mut self,
3682 config: training::SelfPlayConfig,
3683 iterations: usize,
3684 save_path: Option<&str>,
3685 ) -> Result<usize, Box<dyn std::error::Error>> {
3686 let mut total_positions = 0;
3687 let mut trainer = training::SelfPlayTrainer::new(config.clone());
3688
3689 println!("š Starting continuous self-play training for {iterations} iterations...");
3690
3691 for iteration in 1..=iterations {
3692 println!("\n--- Self-Play Iteration {iteration}/{iterations} ---");
3693
3694 let new_data = trainer.generate_training_data(self);
3696 let batch_size = new_data.data.len();
3697
3698 for data in &new_data.data {
3700 self.add_position(&data.board, data.evaluation);
3701 }
3702
3703 total_positions += batch_size;
3704
3705 println!(
3706 "ā
Iteration {}: Added {} positions (total: {})",
3707 iteration,
3708 batch_size,
3709 self.knowledge_base_size()
3710 );
3711
3712 if iteration % 5 == 0 || iteration == iterations {
3714 if let Some(path) = save_path {
3716 match self.save_training_data_binary(path) {
3717 Ok(_) => println!("š¾ Progress saved to {path} (binary format)"),
3718 Err(_e) => println!("Loading complete"),
3719 }
3720 }
3721
3722 if self.database.is_some() {
3724 match self.save_to_database() {
3725 Ok(_) => println!(
3726 "š¾ Database synchronized ({} total positions)",
3727 self.knowledge_base_size()
3728 ),
3729 Err(_e) => println!("Loading complete"),
3730 }
3731 }
3732 }
3733
3734 if iteration % 10 == 0
3736 && self.knowledge_base_size() > 5000
3737 && self.manifold_learner.is_some()
3738 {
3739 println!("š§ Retraining manifold learning with new data...");
3740 let _ = self.train_manifold_learning(5);
3741 }
3742 }
3743
3744 println!("\nš Continuous self-play complete: {total_positions} total new positions");
3745 Ok(total_positions)
3746 }
3747
3748 pub fn adaptive_self_play(
3750 &mut self,
3751 base_config: training::SelfPlayConfig,
3752 target_strength: f32,
3753 ) -> Result<usize, Box<dyn std::error::Error>> {
3754 let mut current_config = base_config;
3755 let mut total_positions = 0;
3756 let mut iteration = 1;
3757
3758 println!(
3759 "šÆ Starting adaptive self-play training (target strength: {target_strength:.2})..."
3760 );
3761
3762 loop {
3763 println!("\n--- Adaptive Iteration {iteration} ---");
3764
3765 let positions_added = self.self_play_training(current_config.clone())?;
3767 total_positions += positions_added;
3768
3769 if self.database.is_some() {
3771 match self.save_to_database() {
3772 Ok(_) => println!("š¾ Adaptive training progress saved to database"),
3773 Err(_e) => println!("Loading complete"),
3774 }
3775 }
3776
3777 let current_strength = self.knowledge_base_size() as f32 / 10000.0; println!(
3781 "š Current strength estimate: {current_strength:.2} (target: {target_strength:.2})"
3782 );
3783
3784 if current_strength >= target_strength {
3785 println!("š Target strength reached!");
3786 break;
3787 }
3788
3789 current_config.exploration_factor *= 0.95; current_config.temperature *= 0.98; current_config.games_per_iteration =
3793 (current_config.games_per_iteration as f32 * 1.1) as usize; iteration += 1;
3796
3797 if iteration > 50 {
3798 println!("ā ļø Maximum iterations reached");
3799 break;
3800 }
3801 }
3802
3803 Ok(total_positions)
3804 }
3805}
3806
3807#[cfg(test)]
3808mod tests {
3809 use super::*;
3810 use chess::Board;
3811
3812 #[test]
3813 fn test_engine_creation() {
3814 let engine = ChessVectorEngine::new(1024);
3815 assert_eq!(engine.knowledge_base_size(), 0);
3816 }
3817
3818 #[test]
3819 fn test_add_and_search() {
3820 let mut engine = ChessVectorEngine::new(1024);
3821 let board = Board::default();
3822
3823 engine.add_position(&board, 0.0);
3824 assert_eq!(engine.knowledge_base_size(), 1);
3825
3826 let similar = engine.find_similar_positions(&board, 1);
3827 assert_eq!(similar.len(), 1);
3828 }
3829
3830 #[test]
3831 fn test_evaluation() {
3832 let mut engine = ChessVectorEngine::new(1024);
3833 let board = Board::default();
3834
3835 engine.add_position(&board, 0.5);
3837
3838 let evaluation = engine.evaluate_position(&board);
3839 assert!(evaluation.is_some());
3840 let eval_value = evaluation.unwrap();
3843 assert!(
3844 eval_value > -1000.0 && eval_value < 1000.0,
3845 "Evaluation should be reasonable: {}",
3846 eval_value
3847 );
3848 }
3849
3850 #[test]
3851 fn test_move_recommendations() {
3852 let mut engine = ChessVectorEngine::new(1024);
3853 let board = Board::default();
3854
3855 use chess::ChessMove;
3857 use std::str::FromStr;
3858 let mov = ChessMove::from_str("e2e4").unwrap();
3859 engine.add_position_with_move(&board, 0.0, Some(mov), Some(0.8));
3860
3861 let recommendations = engine.recommend_moves(&board, 3);
3862 assert!(!recommendations.is_empty());
3863
3864 let legal_recommendations = engine.recommend_legal_moves(&board, 3);
3866 assert!(!legal_recommendations.is_empty());
3867 }
3868
3869 #[test]
3870 fn test_empty_knowledge_base_fallback() {
3871 let mut engine = ChessVectorEngine::new(1024);
3873
3874 use std::str::FromStr;
3876 let board =
3877 Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 0 1")
3878 .unwrap();
3879
3880 let recommendations = engine.recommend_moves(&board, 5);
3882 assert!(
3883 !recommendations.is_empty(),
3884 "recommend_moves should not return empty even with no training data"
3885 );
3886 assert_eq!(
3887 recommendations.len(),
3888 5,
3889 "Should return exactly 5 recommendations"
3890 );
3891
3892 for rec in &recommendations {
3894 assert!(rec.confidence > 0.0, "Confidence should be greater than 0");
3895 assert_eq!(
3896 rec.from_similar_position_count, 1,
3897 "Should have count of 1 for fallback"
3898 );
3899 assert!(
3902 rec.average_outcome.abs() < 1000.0,
3903 "Average outcome should be reasonable: {}",
3904 rec.average_outcome
3905 );
3906 }
3907
3908 let starting_board = Board::default();
3910 let starting_recommendations = engine.recommend_moves(&starting_board, 3);
3911 assert!(
3912 !starting_recommendations.is_empty(),
3913 "Should work for starting position too"
3914 );
3915
3916 use chess::MoveGen;
3918 let legal_moves: std::collections::HashSet<_> = MoveGen::new_legal(&board).collect();
3919 for rec in &recommendations {
3920 assert!(
3921 legal_moves.contains(&rec.chess_move),
3922 "All recommended moves should be legal"
3923 );
3924 }
3925 }
3926
3927 #[test]
3928 fn test_opening_book_integration() {
3929 let mut engine = ChessVectorEngine::new(1024);
3930
3931 engine.enable_opening_book();
3933 assert!(engine.opening_book.is_some());
3934
3935 let board = Board::default();
3937 assert!(engine.is_opening_position(&board));
3938
3939 let entry = engine.get_opening_entry(&board);
3940 assert!(entry.is_some());
3941
3942 let stats = engine.opening_book_stats();
3943 assert!(stats.is_some());
3944 assert!(stats.unwrap().total_openings > 0);
3945
3946 let recommendations = engine.recommend_moves(&board, 3);
3948 assert!(!recommendations.is_empty());
3949 assert!(recommendations[0].confidence > 0.7); }
3951
3952 #[test]
3953 fn test_manifold_learning_integration() {
3954 let mut engine = ChessVectorEngine::new(1024);
3955
3956 let board = Board::default();
3958 for i in 0..10 {
3959 engine.add_position(&board, i as f32 * 0.1);
3960 }
3961
3962 assert!(engine.enable_manifold_learning(8.0).is_ok());
3964
3965 let ratio = engine.manifold_compression_ratio();
3967 assert!(ratio.is_some());
3968 assert!((ratio.unwrap() - 8.0).abs() < 0.1);
3969
3970 assert!(engine.train_manifold_learning(5).is_ok());
3972
3973 let original_similar = engine.find_similar_positions(&board, 3);
3975 assert!(!original_similar.is_empty());
3976 }
3977
3978 #[test]
3979 fn test_lsh_integration() {
3980 let mut engine = ChessVectorEngine::new(1024);
3981
3982 let board = Board::default();
3984 for i in 0..50 {
3985 engine.add_position(&board, i as f32 * 0.02);
3986 }
3987
3988 engine.enable_lsh(4, 8);
3990
3991 let similar = engine.find_similar_positions(&board, 5);
3993 assert!(!similar.is_empty());
3994 assert!(similar.len() <= 5);
3995
3996 let eval = engine.evaluate_position(&board);
3998 assert!(eval.is_some());
3999 }
4000
4001 #[test]
4002 fn test_manifold_lsh_integration() {
4003 let mut engine = ChessVectorEngine::new(1024);
4004
4005 let board = Board::default();
4007 for i in 0..20 {
4008 engine.add_position(&board, i as f32 * 0.05);
4009 }
4010
4011 assert!(engine.enable_manifold_learning(8.0).is_ok());
4013 assert!(engine.train_manifold_learning(3).is_ok());
4014
4015 assert!(engine.enable_manifold_lsh(4, 8).is_ok());
4017
4018 let similar = engine.find_similar_positions(&board, 3);
4020 assert!(!similar.is_empty());
4021
4022 let _recommendations = engine.recommend_moves(&board, 2);
4024 }
4026
4027 #[test]
4052 fn test_position_with_move_storage() {
4053 let mut engine = ChessVectorEngine::new(1024);
4054 let board = Board::default();
4055
4056 use chess::ChessMove;
4057 use std::str::FromStr;
4058 let move1 = ChessMove::from_str("e2e4").unwrap();
4059 let move2 = ChessMove::from_str("d2d4").unwrap();
4060
4061 engine.add_position_with_move(&board, 0.0, Some(move1), Some(0.7));
4063 engine.add_position_with_move(&board, 0.1, Some(move2), Some(0.6));
4064
4065 assert_eq!(engine.position_moves.len(), 2);
4067
4068 let recommendations = engine.recommend_moves(&board, 5);
4070 let _move_strings: Vec<String> = recommendations
4071 .iter()
4072 .map(|r| r.chess_move.to_string())
4073 .collect();
4074
4075 assert!(!recommendations.is_empty());
4077 }
4078
4079 #[test]
4080 fn test_performance_regression_basic() {
4081 use std::time::Instant;
4082
4083 let mut engine = ChessVectorEngine::new(1024);
4084 let board = Board::default();
4085
4086 for i in 0..100 {
4088 engine.add_position(&board, i as f32 * 0.01);
4089 }
4090
4091 let start = Instant::now();
4093
4094 for _ in 0..100 {
4096 engine.add_position(&board, 0.0);
4097 }
4098
4099 let encoding_time = start.elapsed();
4100
4101 let start = Instant::now();
4103 for _ in 0..10 {
4104 engine.find_similar_positions(&board, 5);
4105 }
4106 let search_time = start.elapsed();
4107
4108 assert!(
4110 encoding_time.as_millis() < 10000,
4111 "Position encoding too slow: {}ms",
4112 encoding_time.as_millis()
4113 );
4114 assert!(
4115 search_time.as_millis() < 5000,
4116 "Search too slow: {}ms",
4117 search_time.as_millis()
4118 );
4119 }
4120
4121 #[test]
4122 fn test_memory_usage_reasonable() {
4123 let mut engine = ChessVectorEngine::new(1024);
4124 let board = Board::default();
4125
4126 let initial_size = engine.knowledge_base_size();
4128
4129 for i in 0..1000 {
4130 engine.add_position(&board, i as f32 * 0.001);
4131 }
4132
4133 let final_size = engine.knowledge_base_size();
4134 assert_eq!(final_size, initial_size + 1000);
4135
4136 assert!(final_size > initial_size);
4138 }
4139
4140 #[test]
4141 fn test_incremental_training() {
4142 use std::str::FromStr;
4143
4144 let mut engine = ChessVectorEngine::new(1024);
4145 let board1 = Board::default();
4146 let board2 =
4147 Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap();
4148
4149 engine.add_position(&board1, 0.0);
4151 engine.add_position(&board2, 0.2);
4152 assert_eq!(engine.knowledge_base_size(), 2);
4153
4154 let mut dataset = crate::training::TrainingDataset::new();
4156 dataset.add_position(board1, 0.1, 15, 1); dataset.add_position(
4158 Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
4159 .unwrap(),
4160 0.3,
4161 15,
4162 2,
4163 ); engine.train_from_dataset_incremental(&dataset);
4167
4168 assert_eq!(engine.knowledge_base_size(), 3);
4170
4171 let stats = engine.training_stats();
4173 assert_eq!(stats.total_positions, 3);
4174 assert_eq!(stats.unique_positions, 3);
4175 assert!(!stats.has_move_data); }
4177
4178 #[test]
4179 fn test_save_load_incremental() {
4180 use std::str::FromStr;
4181 use tempfile::tempdir;
4182
4183 let temp_dir = tempdir().unwrap();
4184 let file_path = temp_dir.path().join("test_training.json");
4185
4186 let mut engine1 = ChessVectorEngine::new(1024);
4188 engine1.add_position(&Board::default(), 0.0);
4189 engine1.add_position(
4190 &Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap(),
4191 0.2,
4192 );
4193
4194 engine1.save_training_data(&file_path).unwrap();
4196
4197 let mut engine2 = ChessVectorEngine::new(1024);
4199 engine2.add_position(
4200 &Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
4201 .unwrap(),
4202 0.3,
4203 );
4204 assert_eq!(engine2.knowledge_base_size(), 1);
4205
4206 engine2.load_training_data_incremental(&file_path).unwrap();
4208
4209 assert_eq!(engine2.knowledge_base_size(), 3);
4211 }
4212
4213 #[test]
4214 fn test_training_stats() {
4215 use std::str::FromStr;
4216
4217 let mut engine = ChessVectorEngine::new(1024);
4218
4219 let stats = engine.training_stats();
4221 assert_eq!(stats.total_positions, 0);
4222 assert_eq!(stats.unique_positions, 0);
4223 assert!(!stats.has_move_data);
4224 assert!(!stats.lsh_enabled);
4225 assert!(!stats.manifold_enabled);
4226 assert!(!stats.opening_book_enabled);
4227
4228 engine.add_position(&Board::default(), 0.0);
4230 engine.add_position_with_move(
4231 &Board::default(),
4232 0.1,
4233 Some(ChessMove::from_str("e2e4").unwrap()),
4234 Some(0.8),
4235 );
4236
4237 engine.enable_opening_book();
4239 engine.enable_lsh(4, 8);
4240
4241 let stats = engine.training_stats();
4242 assert_eq!(stats.total_positions, 2);
4243 assert!(stats.has_move_data);
4244 assert!(stats.move_data_entries > 0);
4245 assert!(stats.lsh_enabled);
4246 assert!(stats.opening_book_enabled);
4247 }
4248
4249 #[test]
4250 fn test_tactical_search_integration() {
4251 let mut engine = ChessVectorEngine::new(1024);
4252 let board = Board::default();
4253
4254 assert!(engine.is_tactical_search_enabled());
4256
4257 engine.enable_tactical_search_default();
4259 assert!(engine.is_tactical_search_enabled());
4260
4261 let evaluation = engine.evaluate_position(&board);
4263 assert!(evaluation.is_some());
4264
4265 engine.add_position(&board, 0.5);
4267 let hybrid_evaluation = engine.evaluate_position(&board);
4268 assert!(hybrid_evaluation.is_some());
4269 }
4270
4271 #[test]
4272 fn test_hybrid_evaluation_configuration() {
4273 let mut engine = ChessVectorEngine::new(1024);
4274 let board = Board::default();
4275
4276 engine.enable_tactical_search_default();
4278
4279 let custom_config = HybridConfig {
4281 pattern_confidence_threshold: 0.9, enable_tactical_refinement: true,
4283 tactical_config: TacticalConfig::default(),
4284 pattern_weight: 0.8,
4285 min_similar_positions: 5,
4286 };
4287
4288 engine.configure_hybrid_evaluation(custom_config);
4289
4290 engine.add_position(&board, 0.3);
4292
4293 let evaluation = engine.evaluate_position(&board);
4294 assert!(evaluation.is_some());
4295
4296 let no_tactical_config = HybridConfig {
4298 enable_tactical_refinement: false,
4299 ..HybridConfig::default()
4300 };
4301
4302 engine.configure_hybrid_evaluation(no_tactical_config);
4303
4304 let pattern_only_evaluation = engine.evaluate_position(&board);
4305 assert!(pattern_only_evaluation.is_some());
4306 }
4307}