1pub mod errors;
54pub mod utils;
55
56pub use errors::ChessEngineError;
58
59pub mod core_evaluation;
60pub mod auto_discovery;
61pub mod gpu_acceleration;
62pub mod lichess_loader;
63pub mod lsh;
64pub mod motif_extractor;
65pub mod nnue;
66pub mod opening_book;
67pub mod persistence;
68pub mod position_encoder;
69pub mod similarity_search;
70pub mod strategic_evaluator;
71pub mod strategic_evaluator_lazy;
72pub mod strategic_motifs;
73pub mod streaming_loader;
74pub mod tactical_search;
75pub mod training;
76pub mod ultra_fast_loader;
77pub mod hybrid_evaluation;
79pub mod pattern_recognition;
80pub mod strategic_initiative;
81pub mod evaluation_calibration;
82pub mod stockfish_testing;
83pub mod uci;
84
85pub use auto_discovery::{AutoDiscovery, FormatPriority, TrainingFile};
86pub use gpu_acceleration::{DeviceType, GPUAccelerator};
87pub use lichess_loader::LichessLoader;
88pub use lsh::LSH;
89pub use nnue::{BlendStrategy, EvalStats, HybridEvaluator, NNUEConfig, NNUE};
90pub use opening_book::{OpeningBook, OpeningBookStats, OpeningEntry};
91pub use persistence::{Database, LSHTableData, PositionData};
92pub use position_encoder::PositionEncoder;
93pub use similarity_search::SimilaritySearch;
94pub use strategic_evaluator::{
95 AttackingPattern, PlanGoal, PlanUrgency, PositionalPlan, StrategicConfig, StrategicEvaluation,
96 StrategicEvaluator,
97};
98pub use strategic_evaluator_lazy::{
99 LazyStrategicEvaluator, EnhancedStrategicEvaluation, StrategicThemeAnalysis,
100};
101pub use streaming_loader::StreamingLoader;
102pub use core_evaluation::{
103 CoreEvaluator, CoreEvaluationResult, SimilarityInsights, StrategicInsights,
104 SimilarityEngine, StrategicAnalyzer, EvaluationBlender as CoreEvaluationBlender,
105};
106pub use tactical_search::{TacticalConfig, TacticalResult, TacticalSearch};
107pub use training::{
108 AdvancedSelfLearningSystem, EngineEvaluator, GameExtractor, LearningProgress, LearningStats,
109 SelfPlayConfig, SelfPlayTrainer, TacticalPuzzle, TacticalPuzzleParser, TacticalTrainingData,
110 TrainingData, TrainingDataset,
111};
112pub use ultra_fast_loader::{LoadingStats, UltraFastLoader};
113pub use evaluation_calibration::{
115 CalibrationConfig, CalibratedEvaluator, EvaluationBreakdown, EvaluationComponent,
116 MaterialEvaluator, PieceValues, PositionalEvaluator,
117};
118pub use stockfish_testing::{
119 StockfishTester, StockfishTestConfig, EvaluationComparison, EvaluationCategory,
120 TestSuiteResults, TestStatistics, StockfishTestError,
121};
122pub use hybrid_evaluation::{
123 AdaptiveLearningStats, AnalysisDepth, BlendWeights, ComplexityAnalysisResult,
124 ComplexityAnalyzer, ComplexityCategory, ComplexityFactor, ComplexityWeights, ConfidenceScorer,
125 EvaluationBlender, EvaluationComponent as HybridEvaluationComponent, EvaluationRecommendations,
126 GamePhase as HybridGamePhase, GamePhaseAnalysisResult, GamePhaseDetector,
127 HybridEvaluationEngine, HybridEvaluationResult, HybridEvaluationStats, NNUEEvaluator,
128 PatternEvaluator, PhaseAdaptationRecommendations, PhaseAdaptationSettings,
129 PhaseDetectionWeights, PhaseIndicator, PhaseTransition,
130 StrategicEvaluator as HybridStrategicEvaluator, TacticalEvaluator,
131};
132pub use pattern_recognition::{
133 AdvancedPatternRecognizer, EndgamePatternAnalysis, KingSafetyAnalysis, LearnedPatternMatch,
134 PatternAnalysisResult, PatternRecognitionStats, PatternWeights, PawnStructureAnalysis,
135 PieceCoordinationAnalysis, TacticalPatternAnalysis,
136};
137pub use strategic_initiative::{
138 ColorInitiativeAnalysis, InitiativeAnalyzer, InitiativeFactors, PlanOutcome,
139 PositionalPressure, PositionalPressureEvaluator, StrategicInitiativeEvaluator,
140 StrategicInitiativeResult, StrategicInitiativeStats, StrategicPlan, StrategicPlanGenerator,
141 StrategicPlanType, TimePressure, TimePressureAnalyzer,
142};
143pub use uci::{run_uci_engine, run_uci_engine_with_config, UCIConfig, UCIEngine};
144
145pub use motif_extractor::MotifExtractor;
147pub use strategic_motifs::{
148 CoordinationPattern, EndgamePattern, GamePhase, InitiativePattern, MotifMatch, MotifType,
149 OpeningPattern, PawnPattern, SafetyPattern, StrategicContext, StrategicDatabase,
150 StrategicMotif,
151};
152
153use chess::{Board, ChessMove};
154use ndarray::Array1;
155use serde_json::Value;
156use std::collections::HashMap;
157use std::path::Path;
158use std::str::FromStr;
159
160fn move_centrality(chess_move: &ChessMove) -> f32 {
163 let dest_square = chess_move.get_dest();
164 let rank = dest_square.get_rank().to_index() as f32;
165 let file = dest_square.get_file().to_index() as f32;
166
167 let center_rank = 3.5;
169 let center_file = 3.5;
170
171 let rank_distance = (rank - center_rank).abs();
172 let file_distance = (file - center_file).abs();
173
174 let max_distance = 3.5; let distance = (rank_distance + file_distance) / 2.0;
177 max_distance - distance
178}
179
180#[derive(Debug, Clone)]
182pub struct MoveRecommendation {
183 pub chess_move: ChessMove,
184 pub confidence: f32,
185 pub from_similar_position_count: usize,
186 pub average_outcome: f32,
187}
188
189#[derive(Debug, Clone)]
191pub struct TrainingStats {
192 pub total_positions: usize,
193 pub unique_positions: usize,
194 pub has_move_data: bool,
195 pub move_data_entries: usize,
196 pub lsh_enabled: bool,
197 pub opening_book_enabled: bool,
198}
199
200#[derive(Debug, Clone)]
202pub struct MaterialBalance {
203 pub white_material: f32,
204 pub black_material: f32,
205}
206
207#[derive(Debug, Clone)]
209pub struct HybridConfig {
210 pub pattern_confidence_threshold: f32,
212 pub enable_tactical_refinement: bool,
214 pub tactical_config: TacticalConfig,
216 pub pattern_weight: f32,
218 pub min_similar_positions: usize,
220}
221
222impl Default for HybridConfig {
223 fn default() -> Self {
224 Self {
225 pattern_confidence_threshold: 0.55, enable_tactical_refinement: true,
227 tactical_config: TacticalConfig::default(),
228 pattern_weight: 0.7, min_similar_positions: 2, }
231 }
232}
233
234pub struct ChessVectorEngine {
288 encoder: PositionEncoder,
289 similarity_search: SimilaritySearch,
290 lsh_index: Option<LSH>,
291 use_lsh: bool,
292 position_moves: HashMap<usize, Vec<(ChessMove, f32)>>,
294 position_vectors: Vec<Array1<f32>>,
296 position_boards: Vec<Board>,
298 position_evaluations: Vec<f32>,
300 opening_book: Option<OpeningBook>,
302 database: Option<Database>,
304 tactical_search: Option<TacticalSearch>,
306 hybrid_config: HybridConfig,
310 nnue: Option<NNUE>,
312 strategic_evaluator: Option<StrategicEvaluator>,
314 strategic_database: Option<crate::strategic_motifs::StrategicDatabase>,
316}
317
318impl Clone for ChessVectorEngine {
319 fn clone(&self) -> Self {
320 Self {
321 encoder: self.encoder.clone(),
322 similarity_search: self.similarity_search.clone(),
323 lsh_index: self.lsh_index.clone(),
324 use_lsh: self.use_lsh,
325 position_moves: self.position_moves.clone(),
326 position_vectors: self.position_vectors.clone(),
327 position_boards: self.position_boards.clone(),
328 position_evaluations: self.position_evaluations.clone(),
329 opening_book: self.opening_book.clone(),
330 database: None, tactical_search: self.tactical_search.clone(),
332 hybrid_config: self.hybrid_config.clone(),
334 nnue: None, strategic_evaluator: self.strategic_evaluator.clone(),
336 strategic_database: None, }
338 }
339}
340
341impl ChessVectorEngine {
342 pub fn new(vector_size: usize) -> Self {
345 let mut engine = Self {
346 encoder: PositionEncoder::new(vector_size),
347 similarity_search: SimilaritySearch::new(vector_size),
348 lsh_index: None,
349 use_lsh: false,
350 position_moves: HashMap::new(),
351 position_vectors: Vec::new(),
352 position_boards: Vec::new(),
353 position_evaluations: Vec::new(),
354 opening_book: None,
355 database: None,
356 tactical_search: None,
357 hybrid_config: HybridConfig::default(),
359 nnue: None,
360 strategic_evaluator: None,
361 strategic_database: None,
362 };
363
364 engine.enable_opening_book();
366 engine.enable_tactical_search_default();
367
368 let _ = engine.enable_strategic_motifs();
370
371 engine
372 }
373
374 pub fn new_strong(vector_size: usize) -> Self {
376 let mut engine = Self::new(vector_size);
377 engine.enable_tactical_search(crate::tactical_search::TacticalConfig::strong());
379 engine
380 }
381
382 pub fn new_lightweight(vector_size: usize) -> Self {
384 Self {
385 encoder: PositionEncoder::new(vector_size),
386 similarity_search: SimilaritySearch::new(vector_size),
387 lsh_index: None,
388 use_lsh: false,
389 position_moves: HashMap::new(),
390 position_vectors: Vec::new(),
391 position_boards: Vec::new(),
392 position_evaluations: Vec::new(),
393 opening_book: None,
394 database: None,
395 tactical_search: None, hybrid_config: HybridConfig::default(),
397 nnue: None,
398 strategic_evaluator: None,
399 strategic_database: None,
400 }
401 }
402
403 pub fn new_with_full_database(vector_size: usize) -> Self {
406 let mut engine = Self {
407 encoder: PositionEncoder::new(vector_size),
408 similarity_search: SimilaritySearch::new(vector_size),
409 lsh_index: None,
410 use_lsh: false,
411 position_moves: HashMap::new(),
412 position_vectors: Vec::new(),
413 position_boards: Vec::new(),
414 position_evaluations: Vec::new(),
415 opening_book: None,
416 database: None,
417 tactical_search: None,
418 hybrid_config: HybridConfig::default(),
419 nnue: None,
420 strategic_evaluator: None,
421 strategic_database: None,
422 };
423
424 engine.enable_tactical_search_default();
426 engine
427 }
428
429 pub fn new_adaptive(vector_size: usize, expected_positions: usize, use_case: &str) -> Self {
432 match use_case {
433 "training" => {
434 if expected_positions > 10000 {
435 Self::new_with_lsh(vector_size, 12, 20)
437 } else {
438 Self::new(vector_size)
439 }
440 }
441 "gameplay" => {
442 if expected_positions > 15000 {
443 Self::new_with_lsh(vector_size, 10, 18)
445 } else {
446 Self::new(vector_size)
447 }
448 }
449 "analysis" => {
450 if expected_positions > 10000 {
451 Self::new_with_lsh(vector_size, 14, 22)
453 } else {
454 Self::new(vector_size)
455 }
456 }
457 _ => Self::new(vector_size), }
459 }
460
461 pub fn new_with_lsh(vector_size: usize, num_tables: usize, hash_size: usize) -> Self {
463 Self {
464 encoder: PositionEncoder::new(vector_size),
465 similarity_search: SimilaritySearch::new(vector_size),
466 lsh_index: Some(LSH::new(vector_size, num_tables, hash_size)),
467 use_lsh: true,
468 position_moves: HashMap::new(),
469 position_vectors: Vec::new(),
470 position_boards: Vec::new(),
471 position_evaluations: Vec::new(),
472 opening_book: None,
473 database: None,
474 tactical_search: None,
475 hybrid_config: HybridConfig::default(),
477 nnue: None,
478 strategic_evaluator: None,
479 strategic_database: None,
480 }
481 }
482
483 pub fn enable_lsh(&mut self, num_tables: usize, hash_size: usize) {
485 self.lsh_index = Some(LSH::new(self.encoder.vector_size(), num_tables, hash_size));
486 self.use_lsh = true;
487
488 if let Some(ref mut lsh) = self.lsh_index {
490 for (vector, evaluation) in self.similarity_search.get_all_positions() {
491 lsh.add_vector(vector, evaluation);
492 }
493 }
494 }
495
496 pub fn add_positions_bulk(
498 &mut self,
499 positions: &[(chess::Board, f32, chess::ChessMove)],
500 pb: &indicatif::ProgressBar,
501 ) -> Result<(), Box<dyn std::error::Error>> {
502 use std::collections::HashSet;
503
504 let mut seen_fens = HashSet::new();
506 let mut valid_positions = Vec::new();
507
508 for (board, evaluation, chess_move) in positions {
509 let fen = board.to_string();
510 if !seen_fens.contains(&fen) && self.is_position_safe(board) {
511 seen_fens.insert(fen);
512 valid_positions.push((*board, *evaluation, *chess_move));
513 }
514 }
515
516 println!(
517 "š Filtered {} duplicates, processing {} unique positions",
518 positions.len() - valid_positions.len(),
519 valid_positions.len()
520 );
521
522 pb.set_length(valid_positions.len() as u64);
523
524 let initial_size = self.position_vectors.len();
526
527 self.position_vectors.reserve(valid_positions.len());
528 self.position_boards.reserve(valid_positions.len());
529 self.position_evaluations.reserve(valid_positions.len());
530
531 const CHUNK_SIZE: usize = 10000;
533 let mut processed = 0;
534
535 for chunk in valid_positions.chunks(CHUNK_SIZE) {
536 for (board, evaluation, chess_move) in chunk {
538 let position_index = self.knowledge_base_size();
539
540 self.add_position_fast(board, *evaluation);
542
543 self.position_moves
545 .entry(position_index)
546 .or_default()
547 .push((*chess_move, *evaluation));
548
549 processed += 1;
550 if processed % 1000 == 0 {
551 pb.set_position(processed as u64);
552 }
553 }
554
555 if chunk.len() == CHUNK_SIZE {
557 std::hint::black_box(&self.position_vectors);
559 }
560 }
561
562 pb.finish_with_message("ā
All positions added to knowledge base");
563
564 if self.database.is_some() {
566 println!("š¾ Batch saving {} positions to database...", processed);
567 match self.save_to_database() {
568 Ok(_) => println!("ā
Database save complete"),
569 Err(e) => println!("ā ļø Database save failed: {}", e),
570 }
571 }
572
573 println!(
574 "š Knowledge base grown from {} to {} positions",
575 initial_size,
576 self.knowledge_base_size()
577 );
578
579 Ok(())
580 }
581
582 fn add_position_fast(&mut self, board: &Board, evaluation: f32) {
584 if !self.is_position_safe(board) {
586 return; }
588
589 let vector = self.encoder.encode(board);
590 self.similarity_search
591 .add_position(vector.clone(), evaluation);
592
593 self.position_vectors.push(vector.clone());
595 self.position_boards.push(*board);
596 self.position_evaluations.push(evaluation);
597
598 if let Some(ref mut lsh) = self.lsh_index {
600 lsh.add_vector(vector.clone(), evaluation);
601 }
602
603 }
605
606 pub fn add_position(&mut self, board: &Board, evaluation: f32) {
608 if !self.is_position_safe(board) {
610 return; }
612
613 let vector = self.encoder.encode(board);
614 self.similarity_search
615 .add_position(vector.clone(), evaluation);
616
617 self.position_vectors.push(vector.clone());
619 self.position_boards.push(*board);
620 self.position_evaluations.push(evaluation);
621
622 if let Some(ref mut lsh) = self.lsh_index {
624 lsh.add_vector(vector.clone(), evaluation);
625 }
626
627 if let Some(ref db) = self.database {
629 let current_time = std::time::SystemTime::now()
630 .duration_since(std::time::UNIX_EPOCH)
631 .unwrap_or_default()
632 .as_secs() as i64;
633
634 let position_data = crate::persistence::PositionData {
635 fen: board.to_string(),
636 vector: vector
637 .as_slice()
638 .unwrap()
639 .iter()
640 .map(|&x| x as f64)
641 .collect(),
642 evaluation: Some(evaluation as f64),
643 compressed_vector: None,
644 created_at: current_time,
645 };
646
647 let _ = db.save_position(&position_data);
649 }
650
651 }
652
653 pub fn find_similar_positions(&mut self, board: &Board, k: usize) -> Vec<(Array1<f32>, f32, f32)> {
655 let query_vector = self.encoder.encode(board);
656
657
658 if self.use_lsh {
660 if let Some(ref lsh_index) = self.lsh_index {
661 return lsh_index.query(&query_vector, k);
662 }
663 }
664
665 self.similarity_search.search(&query_vector, k)
667 }
668
669 pub fn find_similar_positions_with_indices(
671 &self,
672 board: &Board,
673 k: usize,
674 ) -> Vec<(usize, f32, f32)> {
675 let query_vector = self.encoder.encode(board);
676
677 let mut results = Vec::new();
680
681 for (i, stored_vector) in self.position_vectors.iter().enumerate() {
682 let similarity = self.encoder.similarity(&query_vector, stored_vector);
683 let eval = self.position_evaluations.get(i).copied().unwrap_or(0.0);
684 results.push((i, eval, similarity));
685 }
686
687 results.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
689 results.truncate(k);
690
691 results
692 }
693
694 pub fn evaluate_position(&mut self, board: &Board) -> Option<f32> {
697 if let Some(entry) = self.get_opening_entry(board) {
706 return Some(entry.evaluation);
707 }
708
709 let strategic_motif_eval = self.get_strategic_motif_evaluation(board);
711
712 let position_complexity = self.calculate_position_complexity(board);
714 let material_balance = self.calculate_material_balance(board);
715 let material_deficit =
716 (material_balance.white_material - material_balance.black_material).abs();
717
718 let strategic_motif_confidence = if strategic_motif_eval.abs() > 0.1 {
720 let mut base_confidence = 0.85;
721
722 if material_deficit > 3.0 {
724 base_confidence *= 0.2; } else if material_deficit > 1.0 {
726 base_confidence *= 0.5; }
728
729 if position_complexity > 0.7 {
731 base_confidence *= 0.3; } else if position_complexity > 0.4 {
733 base_confidence *= 0.6; }
735
736 base_confidence
737 } else {
738 0.0
739 };
740
741 let nnue_evaluation = if let Some(ref mut nnue) = self.nnue {
743 nnue.evaluate(board).ok()
744 } else {
745 None
746 };
747
748 let similar_positions = self.find_similar_positions(board, 10);
751
752 let (pattern_evaluation, pattern_confidence) = if similar_positions.is_empty() {
754 (0.0, 0.0)
756 } else {
757 let mut weighted_sum = 0.0;
759 let mut weight_sum = 0.0;
760 let mut similarity_scores = Vec::new();
761
762 for (_, evaluation, similarity) in &similar_positions {
763 let weight = *similarity;
764 weighted_sum += evaluation * weight;
765 weight_sum += weight;
766 similarity_scores.push(*similarity);
767 }
768
769 let pattern_eval = weighted_sum / weight_sum;
770
771 let avg_similarity =
773 similarity_scores.iter().sum::<f32>() / similarity_scores.len() as f32;
774 let count_factor = (similar_positions.len() as f32
775 / self.hybrid_config.min_similar_positions as f32)
776 .min(1.0);
777 let confidence = avg_similarity * count_factor;
778
779 (pattern_eval, confidence)
780 };
781
782 let material_balance = self.calculate_material_balance(board);
784 let material_deficit =
785 (material_balance.white_material - material_balance.black_material).abs();
786 let position_criticality =
787 self.calculate_position_criticality(board, material_deficit, position_complexity);
788 let (adaptive_depth, search_time_ms) = self.calculate_adaptive_search_parameters(
789 position_criticality,
790 material_deficit,
791 position_complexity,
792 );
793
794 let has_king_danger = self.has_king_danger(board);
796
797 if let Some(ref mut tactical_search) = self.tactical_search {
799 let original_config = tactical_search.config.clone();
801
802 tactical_search.config.max_depth = adaptive_depth;
804 tactical_search.config.max_time_ms = search_time_ms;
805
806 if position_criticality > 0.8 {
808 tactical_search.config.enable_quiescence = true;
809 tactical_search.config.quiescence_depth = 12;
810 tactical_search.config.enable_principal_variation_search = true;
811 }
812
813 tactical_search.config.enable_parallel_search = adaptive_depth >= 8;
815
816 let tactical_result = if tactical_search.config.enable_parallel_search {
820 tactical_search.search_parallel(board)
821 } else {
822 tactical_search.search(board)
823 };
824
825 let is_tactically_dangerous = tactical_result.evaluation.abs() > 1.5
827 || board.checkers().popcnt() > 0
828 || has_king_danger
829 || tactical_result.is_tactical;
830
831 let final_tactical_result = if !similar_positions.is_empty() && !is_tactically_dangerous
832 {
833 tactical_search.search_with_pattern_data(
835 board,
836 Some(pattern_evaluation),
837 pattern_confidence,
838 )
839 } else {
840 tactical_result
842 };
843
844 let mut final_evaluation = final_tactical_result.evaluation;
846
847 if nnue_evaluation.is_some() {
849 if let Some(ref mut nnue) = self.nnue {
850 if let Ok(nnue_hybrid_eval) =
851 nnue.evaluate_hybrid(board, Some(pattern_evaluation), None)
852 {
853 let nnue_weight = if pattern_confidence > 0.7 {
855 0.3 } else if pattern_confidence > 0.4 {
857 0.2 } else {
859 0.1 };
861
862 final_evaluation = (final_evaluation * (1.0 - nnue_weight))
863 + (nnue_hybrid_eval * nnue_weight);
864 }
865 }
866 }
867
868 let evaluation_disagreement = (final_evaluation - strategic_motif_eval).abs();
870
871 if evaluation_disagreement > 1.5 {
872 if material_deficit > 2.0 {
874 let tactical_weight = 0.8; final_evaluation = (final_evaluation * tactical_weight)
877 + (strategic_motif_eval * (1.0 - tactical_weight));
878 } else if position_complexity > 0.6 {
879 let tactical_weight = 0.7; final_evaluation = (final_evaluation * tactical_weight)
882 + (strategic_motif_eval * (1.0 - tactical_weight));
883 } else {
884 final_evaluation = (final_evaluation * 0.6) + (strategic_motif_eval * 0.4);
886 }
887 } else {
888 if strategic_motif_confidence > 0.7 {
890 let strategic_weight = 0.6; final_evaluation = (final_evaluation * (1.0 - strategic_weight))
893 + (strategic_motif_eval * strategic_weight);
894 } else if strategic_motif_eval.abs() > 0.05 {
895 let strategic_weight = 0.3; final_evaluation = (final_evaluation * (1.0 - strategic_weight))
898 + (strategic_motif_eval * strategic_weight);
899 }
900 }
901
902 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
904 final_evaluation = strategic_evaluator.blend_with_hybrid_evaluation(
905 board,
906 nnue_evaluation.unwrap_or(final_evaluation),
907 pattern_evaluation,
908 );
909 }
910
911 tactical_search.config = original_config;
913
914 Some(final_evaluation)
915 } else {
916 if strategic_motif_confidence > 0.5 {
918 let mut final_evaluation = strategic_motif_eval;
920
921 if !similar_positions.is_empty() {
923 final_evaluation = (strategic_motif_eval * 0.7) + (pattern_evaluation * 0.3);
924 }
925
926 if let Some(nnue_eval) = nnue_evaluation {
928 final_evaluation = (final_evaluation * 0.8) + (nnue_eval * 0.2);
929 }
930
931 Some(final_evaluation)
932 } else if !similar_positions.is_empty() {
933 let mut final_evaluation = pattern_evaluation;
935
936 if strategic_motif_eval.abs() > 0.05 {
938 final_evaluation = (pattern_evaluation * 0.7) + (strategic_motif_eval * 0.3);
939 }
940
941 if nnue_evaluation.is_some() {
943 if let Some(ref mut nnue) = self.nnue {
944 if let Ok(nnue_hybrid_eval) =
945 nnue.evaluate_hybrid(board, Some(pattern_evaluation), None)
946 {
947 final_evaluation = (final_evaluation * 0.7) + (nnue_hybrid_eval * 0.3);
948 }
949 }
950 }
951
952 Some(final_evaluation)
953 } else {
954 if strategic_motif_eval.abs() > 0.05 {
956 if let Some(nnue_eval) = nnue_evaluation {
958 Some((strategic_motif_eval * 0.6) + (nnue_eval * 0.4))
959 } else {
960 Some(strategic_motif_eval)
961 }
962 } else {
963 nnue_evaluation
964 }
965 }
966 }
967 }
968
969 pub fn evaluate_position_simplified(&mut self, board: &Board) -> crate::core_evaluation::CoreEvaluationResult {
975 use crate::core_evaluation::CoreEvaluator;
976
977 let mut core_evaluator = CoreEvaluator::new();
979
980 for (i, board_ref) in self.position_boards.iter().enumerate() {
983 if i < self.position_evaluations.len() {
984 core_evaluator.learn_from_position(board_ref, self.position_evaluations[i]);
985 }
986 }
987
988 core_evaluator.evaluate_position(board)
990 }
991
992 pub fn encode_position(&self, board: &Board) -> Array1<f32> {
994 self.encoder.encode(board)
995 }
996
997 pub fn calculate_similarity(&self, board1: &Board, board2: &Board) -> f32 {
999 let vec1 = self.encoder.encode(board1);
1000 let vec2 = self.encoder.encode(board2);
1001 self.encoder.similarity(&vec1, &vec2)
1002 }
1003
1004 pub fn knowledge_base_size(&self) -> usize {
1006 self.similarity_search.size()
1007 }
1008
1009 pub fn get_position_ref(&self, index: usize) -> Option<(&Array1<f32>, f32)> {
1011 self.similarity_search.get_position_ref(index)
1012 }
1013
1014 pub fn get_board_by_index(&self, index: usize) -> Option<&Board> {
1016 self.position_boards.get(index)
1017 }
1018
1019 pub fn get_evaluation_by_index(&self, index: usize) -> Option<f32> {
1021 self.position_evaluations.get(index).copied()
1022 }
1023
1024 pub fn load_strategic_database<P: AsRef<std::path::Path>>(
1026 &mut self,
1027 path: P,
1028 ) -> Result<(), Box<dyn std::error::Error>> {
1029 println!("šÆ Loading strategic motif database...");
1030 let start_time = std::time::Instant::now();
1031
1032 let database = crate::strategic_motifs::StrategicDatabase::load_from_binary(path)?;
1033 let load_time = start_time.elapsed();
1034
1035 let stats = database.stats();
1036 println!(
1037 "ā
Strategic database loaded in {:.0}ms",
1038 load_time.as_millis()
1039 );
1040 println!(" š {} strategic motifs available", stats.total_motifs);
1041
1042 self.strategic_database = Some(database);
1043 Ok(())
1044 }
1045
1046 pub fn get_strategic_motif_evaluation(&mut self, board: &Board) -> f32 {
1049 if let Some(ref mut strategic_db) = self.strategic_database {
1050 let base_strategic_eval = strategic_db.get_strategic_evaluation(board);
1051
1052 let material_balance = self.calculate_material_balance(board);
1054 let material_deficit =
1055 (material_balance.white_material - material_balance.black_material).abs();
1056
1057 let mut adjusted_eval = base_strategic_eval;
1059
1060 if material_deficit > 3.0 {
1062 adjusted_eval *= 0.3;
1064 let material_penalty = material_deficit * 0.8;
1066 if board.side_to_move() == chess::Color::White {
1067 if material_balance.white_material < material_balance.black_material {
1068 adjusted_eval -= material_penalty;
1069 } else {
1070 adjusted_eval += material_penalty;
1071 }
1072 } else {
1073 if material_balance.black_material < material_balance.white_material {
1074 adjusted_eval -= material_penalty;
1075 } else {
1076 adjusted_eval += material_penalty;
1077 }
1078 }
1079 } else if material_deficit > 1.0 {
1080 adjusted_eval *= 0.6;
1082 let material_penalty = material_deficit * 0.5;
1083 if board.side_to_move() == chess::Color::White {
1084 if material_balance.white_material < material_balance.black_material {
1085 adjusted_eval -= material_penalty;
1086 } else {
1087 adjusted_eval += material_penalty;
1088 }
1089 } else {
1090 if material_balance.black_material < material_balance.white_material {
1091 adjusted_eval -= material_penalty;
1092 } else {
1093 adjusted_eval += material_penalty;
1094 }
1095 }
1096 }
1097
1098 adjusted_eval
1099 } else {
1100 0.0 }
1102 }
1103
1104 fn calculate_material_balance(&self, board: &Board) -> MaterialBalance {
1106 let mut white_material = 0.0;
1107 let mut black_material = 0.0;
1108
1109 for square in chess::ALL_SQUARES {
1110 if let Some(piece) = board.piece_on(square) {
1111 let value = match piece {
1112 chess::Piece::Pawn => 1.0,
1113 chess::Piece::Knight => 3.0,
1114 chess::Piece::Bishop => 3.0,
1115 chess::Piece::Rook => 5.0,
1116 chess::Piece::Queen => 9.0,
1117 chess::Piece::King => 0.0,
1118 };
1119
1120 if board.color_on(square) == Some(chess::Color::White) {
1121 white_material += value;
1122 } else {
1123 black_material += value;
1124 }
1125 }
1126 }
1127
1128 MaterialBalance {
1129 white_material,
1130 black_material,
1131 }
1132 }
1133
1134 fn calculate_position_complexity(&self, board: &Board) -> f32 {
1136 let mut complexity = 0.0;
1137
1138 if *board.checkers() != chess::EMPTY {
1140 complexity += 0.4; }
1142
1143 let mut attacked_pieces = 0;
1145 for square in chess::ALL_SQUARES {
1146 if let Some(_piece) = board.piece_on(square) {
1147 if self.is_square_attacked(board, square, !board.side_to_move()) {
1149 attacked_pieces += 1;
1150 }
1151 }
1152 }
1153 complexity += (attacked_pieces as f32) * 0.05;
1154
1155 let legal_moves = chess::MoveGen::new_legal(board).count();
1157 complexity += (legal_moves as f32 - 20.0) * 0.01; let material_balance = self.calculate_material_balance(board);
1161 let material_imbalance =
1162 (material_balance.white_material - material_balance.black_material).abs();
1163 complexity += material_imbalance * 0.02;
1164
1165 complexity.max(0.0).min(1.0)
1167 }
1168
1169 fn calculate_adaptive_search_depth(
1171 &self,
1172 complexity: f32,
1173 material_deficit: f32,
1174 board: &Board,
1175 ) -> u32 {
1176 let mut base_depth = 4u32; if complexity > 0.7 {
1180 base_depth += 4; } else if complexity > 0.4 {
1182 base_depth += 2; }
1184
1185 if material_deficit > 3.0 {
1187 base_depth += 3; } else if material_deficit > 1.0 {
1189 base_depth += 1; }
1191
1192 if *board.checkers() != chess::EMPTY {
1194 base_depth += 2; }
1196
1197 let total_material = self.calculate_material_balance(board);
1199 let total_pieces = total_material.white_material + total_material.black_material;
1200 if total_pieces < 20.0 {
1201 base_depth += 2; }
1203
1204 base_depth.max(3).min(12)
1206 }
1207
1208 fn is_square_attacked(
1210 &self,
1211 board: &Board,
1212 square: chess::Square,
1213 by_color: chess::Color,
1214 ) -> bool {
1215 let _test_board = board.clone();
1217
1218 let attacking_pieces = board.color_combined(by_color);
1224
1225 for attacking_square in attacking_pieces.into_iter() {
1226 if let Some(piece) = board.piece_on(attacking_square) {
1227 if board.color_on(attacking_square) == Some(by_color) {
1228 if self.piece_can_attack_square(board, attacking_square, piece, square) {
1229 return true;
1230 }
1231 }
1232 }
1233 }
1234
1235 false
1236 }
1237
1238 fn piece_can_attack_square(
1240 &self,
1241 board: &Board,
1242 piece_square: chess::Square,
1243 piece: chess::Piece,
1244 target_square: chess::Square,
1245 ) -> bool {
1246 match piece {
1247 chess::Piece::Pawn => self.pawn_can_attack_square(board, piece_square, target_square),
1248 chess::Piece::Knight => self.knight_can_attack_square(piece_square, target_square),
1249 chess::Piece::Bishop => {
1250 self.bishop_can_attack_square(board, piece_square, target_square)
1251 }
1252 chess::Piece::Rook => self.rook_can_attack_square(board, piece_square, target_square),
1253 chess::Piece::Queen => self.queen_can_attack_square(board, piece_square, target_square),
1254 chess::Piece::King => self.king_can_attack_square(piece_square, target_square),
1255 }
1256 }
1257
1258 fn pawn_can_attack_square(
1260 &self,
1261 board: &Board,
1262 pawn_square: chess::Square,
1263 target_square: chess::Square,
1264 ) -> bool {
1265 let pawn_color = board.color_on(pawn_square).unwrap();
1266 let pawn_rank = pawn_square.get_rank().to_index() as i8;
1267 let pawn_file = pawn_square.get_file().to_index() as i8;
1268 let target_rank = target_square.get_rank().to_index() as i8;
1269 let target_file = target_square.get_file().to_index() as i8;
1270
1271 let direction = if pawn_color == chess::Color::White {
1272 1
1273 } else {
1274 -1
1275 };
1276
1277 if target_rank == pawn_rank + direction {
1279 if (target_file - pawn_file).abs() == 1 {
1280 return true;
1281 }
1282 }
1283
1284 false
1285 }
1286
1287 fn knight_can_attack_square(
1289 &self,
1290 knight_square: chess::Square,
1291 target_square: chess::Square,
1292 ) -> bool {
1293 let knight_rank = knight_square.get_rank().to_index() as i8;
1294 let knight_file = knight_square.get_file().to_index() as i8;
1295 let target_rank = target_square.get_rank().to_index() as i8;
1296 let target_file = target_square.get_file().to_index() as i8;
1297
1298 let rank_diff = (target_rank - knight_rank).abs();
1299 let file_diff = (target_file - knight_file).abs();
1300
1301 (rank_diff == 2 && file_diff == 1) || (rank_diff == 1 && file_diff == 2)
1303 }
1304
1305 fn bishop_can_attack_square(
1307 &self,
1308 board: &Board,
1309 bishop_square: chess::Square,
1310 target_square: chess::Square,
1311 ) -> bool {
1312 let bishop_rank = bishop_square.get_rank().to_index() as i8;
1313 let bishop_file = bishop_square.get_file().to_index() as i8;
1314 let target_rank = target_square.get_rank().to_index() as i8;
1315 let target_file = target_square.get_file().to_index() as i8;
1316
1317 let rank_diff = target_rank - bishop_rank;
1318 let file_diff = target_file - bishop_file;
1319
1320 if rank_diff.abs() != file_diff.abs() {
1322 return false;
1323 }
1324
1325 let rank_step = rank_diff.signum();
1327 let file_step = file_diff.signum();
1328
1329 let mut check_rank = bishop_rank + rank_step;
1330 let mut check_file = bishop_file + file_step;
1331
1332 while check_rank != target_rank {
1333 let check_square = chess::Square::make_square(
1334 chess::Rank::from_index(check_rank as usize),
1335 chess::File::from_index(check_file as usize),
1336 );
1337
1338 if board.piece_on(check_square).is_some() {
1339 return false; }
1341
1342 check_rank += rank_step;
1343 check_file += file_step;
1344 }
1345
1346 true
1347 }
1348
1349 fn rook_can_attack_square(
1351 &self,
1352 board: &Board,
1353 rook_square: chess::Square,
1354 target_square: chess::Square,
1355 ) -> bool {
1356 let rook_rank = rook_square.get_rank().to_index() as i8;
1357 let rook_file = rook_square.get_file().to_index() as i8;
1358 let target_rank = target_square.get_rank().to_index() as i8;
1359 let target_file = target_square.get_file().to_index() as i8;
1360
1361 if rook_rank != target_rank && rook_file != target_file {
1363 return false;
1364 }
1365
1366 if rook_rank == target_rank {
1368 let step = (target_file - rook_file).signum();
1370 let mut check_file = rook_file + step;
1371
1372 while check_file != target_file {
1373 let check_square = chess::Square::make_square(
1374 chess::Rank::from_index(rook_rank as usize),
1375 chess::File::from_index(check_file as usize),
1376 );
1377
1378 if board.piece_on(check_square).is_some() {
1379 return false; }
1381
1382 check_file += step;
1383 }
1384 } else {
1385 let step = (target_rank - rook_rank).signum();
1387 let mut check_rank = rook_rank + step;
1388
1389 while check_rank != target_rank {
1390 let check_square = chess::Square::make_square(
1391 chess::Rank::from_index(check_rank as usize),
1392 chess::File::from_index(target_file as usize),
1393 );
1394
1395 if board.piece_on(check_square).is_some() {
1396 return false; }
1398
1399 check_rank += step;
1400 }
1401 }
1402
1403 true
1404 }
1405
1406 fn queen_can_attack_square(
1408 &self,
1409 board: &Board,
1410 queen_square: chess::Square,
1411 target_square: chess::Square,
1412 ) -> bool {
1413 self.rook_can_attack_square(board, queen_square, target_square)
1415 || self.bishop_can_attack_square(board, queen_square, target_square)
1416 }
1417
1418 fn king_can_attack_square(
1420 &self,
1421 king_square: chess::Square,
1422 target_square: chess::Square,
1423 ) -> bool {
1424 let king_rank = king_square.get_rank().to_index() as i8;
1425 let king_file = king_square.get_file().to_index() as i8;
1426 let target_rank = target_square.get_rank().to_index() as i8;
1427 let target_file = target_square.get_file().to_index() as i8;
1428
1429 let rank_diff = (target_rank - king_rank).abs();
1430 let file_diff = (target_file - king_file).abs();
1431
1432 rank_diff <= 1 && file_diff <= 1 && (rank_diff != 0 || file_diff != 0)
1434 }
1435
1436 pub fn enable_strategic_motifs(&mut self) -> Result<(), Box<dyn std::error::Error>> {
1438 let strategic_paths = ["strategic_motifs.db", "data/strategic_motifs.db"];
1440
1441 for path in &strategic_paths {
1442 if std::path::Path::new(path).exists() {
1443 return self.load_strategic_database(path);
1444 }
1445 }
1446
1447 println!("ā ļø No strategic motif database found");
1448 println!(" Generate with: cargo run --bin extract_strategic_motifs");
1449 Ok(())
1450 }
1451
1452 pub fn save_training_data<P: AsRef<std::path::Path>>(
1454 &self,
1455 path: P,
1456 ) -> Result<(), Box<dyn std::error::Error>> {
1457 use crate::training::{TrainingData, TrainingDataset};
1458
1459 let mut dataset = TrainingDataset::new();
1460
1461 for (i, board) in self.position_boards.iter().enumerate() {
1463 if i < self.position_evaluations.len() {
1464 dataset.data.push(TrainingData {
1465 board: *board,
1466 evaluation: self.position_evaluations[i],
1467 depth: 15, game_id: i, });
1470 }
1471 }
1472
1473 dataset.save_incremental(path)?;
1474 println!("Saved {} positions to training data", dataset.data.len());
1475 Ok(())
1476 }
1477
1478 pub fn load_training_data_incremental<P: AsRef<std::path::Path>>(
1480 &mut self,
1481 path: P,
1482 ) -> Result<(), Box<dyn std::error::Error>> {
1483 use crate::training::TrainingDataset;
1484 use indicatif::{ProgressBar, ProgressStyle};
1485 use std::collections::HashSet;
1486
1487 let existing_size = self.knowledge_base_size();
1488
1489 let path_ref = path.as_ref();
1491 let binary_path = path_ref.with_extension("bin");
1492 if binary_path.exists() {
1493 println!("š Loading optimized binary format...");
1494 return self.load_training_data_binary(binary_path);
1495 }
1496
1497 println!("š Loading training data from {}...", path_ref.display());
1498 let dataset = TrainingDataset::load(path)?;
1499
1500 let total_positions = dataset.data.len();
1501 if total_positions == 0 {
1502 println!("ā ļø No positions found in dataset");
1503 return Ok(());
1504 }
1505
1506 let dedup_pb = ProgressBar::new(total_positions as u64);
1508 dedup_pb.set_style(
1509 ProgressStyle::default_bar()
1510 .template("š Checking duplicates [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}")?
1511 .progress_chars("āāā")
1512 );
1513
1514 let mut existing_boards: HashSet<_> = self.position_boards.iter().cloned().collect();
1516 let mut new_positions = Vec::new();
1517 let mut new_evaluations = Vec::new();
1518
1519 for (i, data) in dataset.data.into_iter().enumerate() {
1521 if !existing_boards.contains(&data.board) {
1522 existing_boards.insert(data.board);
1523 new_positions.push(data.board);
1524 new_evaluations.push(data.evaluation);
1525 }
1526
1527 if i % 1000 == 0 || i == total_positions - 1 {
1528 dedup_pb.set_position((i + 1) as u64);
1529 dedup_pb.set_message(format!("{} new positions found", new_positions.len()));
1530 }
1531 }
1532 dedup_pb.finish_with_message(format!("ā
Found {} new positions", new_positions.len()));
1533
1534 if new_positions.is_empty() {
1535 println!("ā¹ļø No new positions to add (all positions already exist)");
1536 return Ok(());
1537 }
1538
1539 let add_pb = ProgressBar::new(new_positions.len() as u64);
1541 add_pb.set_style(
1542 ProgressStyle::default_bar()
1543 .template("ā Adding positions [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")?
1544 .progress_chars("āāā")
1545 );
1546
1547 for (i, (board, evaluation)) in new_positions
1549 .into_iter()
1550 .zip(new_evaluations.into_iter())
1551 .enumerate()
1552 {
1553 self.add_position(&board, evaluation);
1554
1555 if i % 500 == 0 || i == add_pb.length().unwrap() as usize - 1 {
1556 add_pb.set_position((i + 1) as u64);
1557 add_pb.set_message("vectors encoded".to_string());
1558 }
1559 }
1560 add_pb.finish_with_message("ā
All positions added");
1561
1562 println!(
1563 "šÆ Loaded {} new positions (total: {})",
1564 self.knowledge_base_size() - existing_size,
1565 self.knowledge_base_size()
1566 );
1567 Ok(())
1568 }
1569
1570 pub fn save_training_data_binary<P: AsRef<std::path::Path>>(
1572 &self,
1573 path: P,
1574 ) -> Result<(), Box<dyn std::error::Error>> {
1575 use lz4_flex::compress_prepend_size;
1576
1577 println!("š¾ Saving training data in binary format (compressed)...");
1578
1579 #[derive(serde::Serialize)]
1581 struct BinaryTrainingData {
1582 positions: Vec<String>, evaluations: Vec<f32>,
1584 vectors: Vec<Vec<f32>>, created_at: i64,
1586 }
1587
1588 let current_time = std::time::SystemTime::now()
1589 .duration_since(std::time::UNIX_EPOCH)?
1590 .as_secs() as i64;
1591
1592 let mut positions = Vec::with_capacity(self.position_boards.len());
1594 let mut evaluations = Vec::with_capacity(self.position_boards.len());
1595 let mut vectors = Vec::with_capacity(self.position_boards.len());
1596
1597 for (i, board) in self.position_boards.iter().enumerate() {
1598 if i < self.position_evaluations.len() {
1599 positions.push(board.to_string());
1600 evaluations.push(self.position_evaluations[i]);
1601
1602 if i < self.position_vectors.len() {
1604 if let Some(vector_slice) = self.position_vectors[i].as_slice() {
1605 vectors.push(vector_slice.to_vec());
1606 }
1607 }
1608 }
1609 }
1610
1611 let binary_data = BinaryTrainingData {
1612 positions,
1613 evaluations,
1614 vectors,
1615 created_at: current_time,
1616 };
1617
1618 let serialized = bincode::serialize(&binary_data)?;
1620
1621 let compressed = compress_prepend_size(&serialized);
1623
1624 std::fs::write(path, &compressed)?;
1626
1627 println!(
1628 "ā
Saved {} positions to binary file ({} bytes compressed)",
1629 binary_data.positions.len(),
1630 compressed.len()
1631 );
1632 Ok(())
1633 }
1634
1635 pub fn load_training_data_binary<P: AsRef<std::path::Path>>(
1637 &mut self,
1638 path: P,
1639 ) -> Result<(), Box<dyn std::error::Error>> {
1640 use indicatif::{ProgressBar, ProgressStyle};
1641 use lz4_flex::decompress_size_prepended;
1642 use rayon::prelude::*;
1643
1644 println!("š Loading training data from binary format...");
1645
1646 #[derive(serde::Deserialize)]
1647 struct BinaryTrainingData {
1648 positions: Vec<String>,
1649 evaluations: Vec<f32>,
1650 #[allow(dead_code)]
1651 vectors: Vec<Vec<f32>>,
1652 #[allow(dead_code)]
1653 created_at: i64,
1654 }
1655
1656 let existing_size = self.knowledge_base_size();
1657
1658 let file_size = std::fs::metadata(&path)?.len();
1660 println!(
1661 "š¦ Reading {} compressed file...",
1662 Self::format_bytes(file_size)
1663 );
1664
1665 let compressed_data = std::fs::read(path)?;
1666 println!("š Decompressing data...");
1667 let serialized = decompress_size_prepended(&compressed_data)?;
1668
1669 println!("š Deserializing binary data...");
1670 let binary_data: BinaryTrainingData = bincode::deserialize(&serialized)?;
1671
1672 let total_positions = binary_data.positions.len();
1673 if total_positions == 0 {
1674 println!("ā ļø No positions found in binary file");
1675 return Ok(());
1676 }
1677
1678 println!("š Processing {total_positions} positions from binary format...");
1679
1680 let pb = ProgressBar::new(total_positions as u64);
1682 pb.set_style(
1683 ProgressStyle::default_bar()
1684 .template("ā” Loading positions [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({percent}%) {msg}")?
1685 .progress_chars("āāā")
1686 );
1687
1688 let mut added_count = 0;
1689
1690 if total_positions > 10_000 {
1692 println!("š Using parallel batch processing for large dataset...");
1693
1694 let existing_positions: std::collections::HashSet<_> =
1696 self.position_boards.iter().cloned().collect();
1697
1698 let batch_size = 5000.min(total_positions / num_cpus::get()).max(1000);
1700 let batches: Vec<_> = binary_data
1701 .positions
1702 .chunks(batch_size)
1703 .zip(binary_data.evaluations.chunks(batch_size))
1704 .collect();
1705
1706 println!(
1707 "š Processing {} batches of ~{} positions each...",
1708 batches.len(),
1709 batch_size
1710 );
1711
1712 let valid_positions: Vec<Vec<(Board, f32)>> = batches
1714 .par_iter()
1715 .map(|(fen_batch, eval_batch)| {
1716 let mut batch_positions = Vec::new();
1717
1718 for (fen, &evaluation) in fen_batch.iter().zip(eval_batch.iter()) {
1719 if let Ok(board) = fen.parse::<Board>() {
1720 if !existing_positions.contains(&board) {
1721 let mut eval = evaluation;
1722 if eval.abs() > 15.0 {
1724 eval /= 100.0;
1725 }
1726 batch_positions.push((board, eval));
1727 }
1728 }
1729 }
1730
1731 batch_positions
1732 })
1733 .collect();
1734
1735 for batch in valid_positions {
1737 for (board, evaluation) in batch {
1738 self.add_position(&board, evaluation);
1739 added_count += 1;
1740
1741 if added_count % 1000 == 0 {
1742 pb.set_position(added_count as u64);
1743 pb.set_message(format!("{added_count} new positions"));
1744 }
1745 }
1746 }
1747 } else {
1748 for (i, fen) in binary_data.positions.iter().enumerate() {
1750 if i < binary_data.evaluations.len() {
1751 if let Ok(board) = fen.parse() {
1752 if !self.position_boards.contains(&board) {
1754 let mut evaluation = binary_data.evaluations[i];
1755
1756 if evaluation.abs() > 15.0 {
1758 evaluation /= 100.0;
1759 }
1760
1761 self.add_position(&board, evaluation);
1762 added_count += 1;
1763 }
1764 }
1765 }
1766
1767 if i % 1000 == 0 || i == total_positions - 1 {
1768 pb.set_position((i + 1) as u64);
1769 pb.set_message(format!("{added_count} new positions"));
1770 }
1771 }
1772 }
1773 pb.finish_with_message(format!("ā
Loaded {added_count} new positions"));
1774
1775 println!(
1776 "šÆ Binary loading complete: {} new positions (total: {})",
1777 self.knowledge_base_size() - existing_size,
1778 self.knowledge_base_size()
1779 );
1780 Ok(())
1781 }
1782
1783 pub fn load_training_data_mmap<P: AsRef<Path>>(
1786 &mut self,
1787 path: P,
1788 ) -> Result<(), Box<dyn std::error::Error>> {
1789 use memmap2::Mmap;
1790 use std::fs::File;
1791
1792 let path_ref = path.as_ref();
1793 println!(
1794 "š Loading training data via memory mapping: {}",
1795 path_ref.display()
1796 );
1797
1798 let file = File::open(path_ref)?;
1799 let mmap = unsafe { Mmap::map(&file)? };
1800
1801 if let Ok(data) = rmp_serde::from_slice::<Vec<(String, f32)>>(&mmap) {
1803 println!("š¦ Detected MessagePack format");
1804 return self.load_positions_from_tuples(data);
1805 }
1806
1807 if let Ok(data) = bincode::deserialize::<Vec<(String, f32)>>(&mmap) {
1809 println!("š¦ Detected bincode format");
1810 return self.load_positions_from_tuples(data);
1811 }
1812
1813 let decompressed = lz4_flex::decompress_size_prepended(&mmap)?;
1815 let data: Vec<(String, f32)> = bincode::deserialize(&decompressed)?;
1816 println!("š¦ Detected LZ4+bincode format");
1817 self.load_positions_from_tuples(data)
1818 }
1819
1820 pub fn load_training_data_msgpack<P: AsRef<Path>>(
1823 &mut self,
1824 path: P,
1825 ) -> Result<(), Box<dyn std::error::Error>> {
1826 use std::fs::File;
1827 use std::io::BufReader;
1828
1829 let path_ref = path.as_ref();
1830 println!(
1831 "š Loading MessagePack training data: {}",
1832 path_ref.display()
1833 );
1834
1835 let file = File::open(path_ref)?;
1836 let reader = BufReader::new(file);
1837 let data: Vec<(String, f32)> = rmp_serde::from_read(reader)?;
1838
1839 println!("š¦ MessagePack data loaded: {} positions", data.len());
1840 self.load_positions_from_tuples(data)
1841 }
1842
1843 pub fn load_training_data_streaming_json<P: AsRef<Path>>(
1846 &mut self,
1847 path: P,
1848 ) -> Result<(), Box<dyn std::error::Error>> {
1849 use dashmap::DashMap;
1850 use rayon::prelude::*;
1851 use std::fs::File;
1852 use std::io::{BufRead, BufReader};
1853 use std::sync::Arc;
1854
1855 let path_ref = path.as_ref();
1856 println!(
1857 "š Loading JSON with streaming parallel processing: {}",
1858 path_ref.display()
1859 );
1860
1861 let file = File::open(path_ref)?;
1862 let reader = BufReader::new(file);
1863
1864 let chunk_size = 10000;
1866 let position_map = Arc::new(DashMap::new());
1867
1868 let lines: Vec<String> = reader.lines().collect::<Result<Vec<_>, _>>()?;
1869 let total_lines = lines.len();
1870
1871 lines.par_chunks(chunk_size).for_each(|chunk| {
1873 for line in chunk {
1874 if let Ok(data) = serde_json::from_str::<serde_json::Value>(line) {
1875 if let (Some(fen), Some(eval)) = (
1876 data.get("fen").and_then(|v| v.as_str()),
1877 data.get("evaluation").and_then(|v| v.as_f64()),
1878 ) {
1879 position_map.insert(fen.to_string(), eval as f32);
1880 }
1881 }
1882 }
1883 });
1884
1885 println!(
1886 "š¦ Parallel JSON processing complete: {} positions from {} lines",
1887 position_map.len(),
1888 total_lines
1889 );
1890
1891 let data: Vec<(String, f32)> = match Arc::try_unwrap(position_map) {
1894 Ok(map) => map.into_iter().collect(),
1895 Err(arc_map) => {
1896 arc_map
1898 .iter()
1899 .map(|entry| (entry.key().clone(), *entry.value()))
1900 .collect()
1901 }
1902 };
1903 self.load_positions_from_tuples(data)
1904 }
1905
1906 pub fn load_training_data_compressed<P: AsRef<Path>>(
1909 &mut self,
1910 path: P,
1911 ) -> Result<(), Box<dyn std::error::Error>> {
1912 use std::fs::File;
1913 use std::io::BufReader;
1914
1915 let path_ref = path.as_ref();
1916 println!(
1917 "š Loading zstd compressed training data: {}",
1918 path_ref.display()
1919 );
1920
1921 let file = File::open(path_ref)?;
1922 let reader = BufReader::new(file);
1923 let decoder = zstd::stream::Decoder::new(reader)?;
1924
1925 if let Ok(data) = rmp_serde::from_read::<_, Vec<(String, f32)>>(decoder) {
1927 println!("š¦ Zstd+MessagePack data loaded: {} positions", data.len());
1928 return self.load_positions_from_tuples(data);
1929 }
1930
1931 let file = File::open(path_ref)?;
1933 let reader = BufReader::new(file);
1934 let decoder = zstd::stream::Decoder::new(reader)?;
1935 let data: Vec<(String, f32)> = bincode::deserialize_from(decoder)?;
1936
1937 println!("š¦ Zstd+bincode data loaded: {} positions", data.len());
1938 self.load_positions_from_tuples(data)
1939 }
1940
1941 fn load_positions_from_tuples(
1944 &mut self,
1945 data: Vec<(String, f32)>,
1946 ) -> Result<(), Box<dyn std::error::Error>> {
1947 use indicatif::{ProgressBar, ProgressStyle};
1948 use std::collections::HashSet;
1949
1950 let existing_size = self.knowledge_base_size();
1951 let mut seen_positions = HashSet::new();
1952 let mut loaded_count = 0;
1953
1954 let pb = ProgressBar::new(data.len() as u64);
1956 pb.set_style(ProgressStyle::with_template(
1957 "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({per_sec}) {msg}"
1958 )?);
1959
1960 for (fen, evaluation) in data {
1961 pb.inc(1);
1962
1963 if seen_positions.contains(&fen) {
1965 continue;
1966 }
1967 seen_positions.insert(fen.clone());
1968
1969 if let Ok(board) = Board::from_str(&fen) {
1971 self.add_position(&board, evaluation);
1972 loaded_count += 1;
1973
1974 if loaded_count % 1000 == 0 {
1975 pb.set_message(format!("Loaded {loaded_count} positions"));
1976 }
1977 }
1978 }
1979
1980 pb.finish_with_message(format!("ā
Loaded {loaded_count} new positions"));
1981
1982 println!(
1983 "šÆ Ultra-fast loading complete: {} new positions (total: {})",
1984 self.knowledge_base_size() - existing_size,
1985 self.knowledge_base_size()
1986 );
1987
1988 Ok(())
1989 }
1990
1991 fn format_bytes(bytes: u64) -> String {
1993 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
1994 let mut size = bytes as f64;
1995 let mut unit_index = 0;
1996
1997 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
1998 size /= 1024.0;
1999 unit_index += 1;
2000 }
2001
2002 format!("{:.1} {}", size, UNITS[unit_index])
2003 }
2004
2005 pub fn train_from_dataset_incremental(&mut self, dataset: &crate::training::TrainingDataset) {
2007 let _existing_size = self.knowledge_base_size();
2008 let mut added = 0;
2009
2010 for data in &dataset.data {
2011 if !self.position_boards.contains(&data.board) {
2013 self.add_position(&data.board, data.evaluation);
2014 added += 1;
2015 }
2016 }
2017
2018 println!(
2019 "Added {} new positions from dataset (total: {})",
2020 added,
2021 self.knowledge_base_size()
2022 );
2023 }
2024
2025 pub fn training_stats(&self) -> TrainingStats {
2027 TrainingStats {
2028 total_positions: self.knowledge_base_size(),
2029 unique_positions: self.position_boards.len(),
2030 has_move_data: !self.position_moves.is_empty(),
2031 move_data_entries: self.position_moves.len(),
2032 lsh_enabled: self.use_lsh,
2033 opening_book_enabled: self.opening_book.is_some(),
2034 }
2035 }
2036
2037 pub fn auto_load_training_data(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
2039 use indicatif::{ProgressBar, ProgressStyle};
2040
2041 if std::path::Path::new("chess_vector_engine.db").exists() {
2043 if let Err(_) = self.enable_persistence("chess_vector_engine.db") {
2044 } else if let Ok(_) = self.load_from_database() {
2046 let stats = self.training_stats();
2047 if stats.total_positions > 0 {
2048 println!(
2049 "šļø Auto-loaded engine with {} positions from database!",
2050 stats.total_positions
2051 );
2052 return Ok(vec!["chess_vector_engine.db".to_string()]);
2053 }
2054 }
2055 }
2056
2057 let common_files = vec![
2058 "training_data.json",
2059 "tactical_training_data.json",
2060 "engine_training.json",
2061 "chess_training.json",
2062 "my_training.json",
2063 ];
2064
2065 let tactical_files = vec![
2066 "tactical_puzzles.json",
2067 "lichess_puzzles.json",
2068 "my_puzzles.json",
2069 ];
2070
2071 let mut available_files = Vec::new();
2073 for file_path in &common_files {
2074 if std::path::Path::new(file_path).exists() {
2075 available_files.push((file_path, "training"));
2076 }
2077 }
2078 for file_path in &tactical_files {
2079 if std::path::Path::new(file_path).exists() {
2080 available_files.push((file_path, "tactical"));
2081 }
2082 }
2083
2084 if available_files.is_empty() {
2085 return Ok(Vec::new());
2086 }
2087
2088 println!(
2089 "š Found {} training files to auto-load",
2090 available_files.len()
2091 );
2092
2093 let pb = ProgressBar::new(available_files.len() as u64);
2095 pb.set_style(
2096 ProgressStyle::default_bar()
2097 .template("š Auto-loading files [{elapsed_precise}] [{bar:40.blue/cyan}] {pos}/{len} {msg}")?
2098 .progress_chars("āāā")
2099 );
2100
2101 let mut loaded_files = Vec::new();
2102
2103 for (i, (file_path, file_type)) in available_files.iter().enumerate() {
2104 pb.set_position(i as u64);
2105 pb.set_message("Processing...".to_string());
2106
2107 let result = match *file_type {
2108 "training" => self.load_training_data_incremental(file_path).map(|_| {
2109 loaded_files.push(file_path.to_string());
2110 println!("Loading complete");
2111 }),
2112 "tactical" => crate::training::TacticalPuzzleParser::load_tactical_puzzles(
2113 file_path,
2114 )
2115 .map(|puzzles| {
2116 crate::training::TacticalPuzzleParser::load_into_engine_incremental(
2117 &puzzles, self,
2118 );
2119 loaded_files.push(file_path.to_string());
2120 println!("Loading complete");
2121 }),
2122 _ => Ok(()),
2123 };
2124
2125 if let Err(_e) = result {
2126 println!("Loading complete");
2127 }
2128 }
2129
2130 pb.set_position(available_files.len() as u64);
2131 pb.finish_with_message(format!("ā
Auto-loaded {} files", loaded_files.len()));
2132
2133 Ok(loaded_files)
2134 }
2135
2136 pub fn load_lichess_puzzles<P: AsRef<std::path::Path>>(
2138 &mut self,
2139 csv_path: P,
2140 ) -> Result<(), Box<dyn std::error::Error>> {
2141 println!("š„ Loading Lichess puzzles with enhanced performance...");
2142 let puzzle_entries =
2143 crate::lichess_loader::load_lichess_puzzles_basic_with_moves(csv_path, 100000)?;
2144
2145 for (board, evaluation, best_move) in puzzle_entries {
2146 self.add_position_with_move(&board, evaluation, Some(best_move), Some(evaluation));
2147 }
2148
2149 println!("ā
Lichess puzzle loading complete!");
2150 Ok(())
2151 }
2152
2153 pub fn load_lichess_puzzles_with_limit<P: AsRef<std::path::Path>>(
2155 &mut self,
2156 csv_path: P,
2157 max_puzzles: Option<usize>,
2158 ) -> Result<(), Box<dyn std::error::Error>> {
2159 match max_puzzles {
2160 Some(limit) => {
2161 println!("š Loading Lichess puzzles (limited to {limit} puzzles)...");
2162 let puzzle_entries =
2163 crate::lichess_loader::load_lichess_puzzles_basic_with_moves(csv_path, limit)?;
2164
2165 println!(
2166 "š Adding {} positions to engine knowledge base...",
2167 puzzle_entries.len()
2168 );
2169
2170 use indicatif::{ProgressBar, ProgressStyle};
2172 let pb = ProgressBar::new(puzzle_entries.len() as u64);
2173 pb.set_style(
2174 ProgressStyle::default_bar()
2175 .template("š§ Adding positions [{elapsed_precise}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {per_sec} ETA: {eta}")
2176 .expect("Valid progress template")
2177 .progress_chars("āāā"),
2178 );
2179
2180 self.add_positions_bulk(&puzzle_entries, &pb)?;
2182 }
2183 None => {
2184 self.load_lichess_puzzles(csv_path)?;
2186 return Ok(());
2187 }
2188 }
2189
2190 println!("ā
Lichess puzzle loading complete!");
2191 Ok(())
2192 }
2193
2194 pub fn new_with_auto_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
2196 let mut engine = Self::new(vector_size);
2197 engine.enable_opening_book();
2198
2199 let loaded_files = engine.auto_load_training_data()?;
2201
2202 if loaded_files.is_empty() {
2203 println!("š¤ Created fresh engine (no training data found)");
2204 } else {
2205 println!(
2206 "š Created engine with auto-loaded training data from {} files",
2207 loaded_files.len()
2208 );
2209 let _stats = engine.training_stats();
2210 println!("Loading complete");
2211 println!("Loading complete");
2212 }
2213
2214 Ok(engine)
2215 }
2216
2217 pub fn new_with_fast_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
2220 use indicatif::{ProgressBar, ProgressStyle};
2221
2222 let mut engine = Self::new(vector_size);
2223 engine.enable_opening_book();
2224
2225 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
2227 println!("Loading complete");
2228 }
2229
2230 let binary_files = [
2232 "training_data_a100.bin", "training_data.bin",
2234 "tactical_training_data.bin",
2235 "engine_training.bin",
2236 "chess_training.bin",
2237 ];
2238
2239 let existing_binary_files: Vec<_> = binary_files
2241 .iter()
2242 .filter(|&file_path| std::path::Path::new(file_path).exists())
2243 .collect();
2244
2245 let mut loaded_count = 0;
2246
2247 if !existing_binary_files.is_empty() {
2248 println!(
2249 "ā” Fast loading: Found {} binary files",
2250 existing_binary_files.len()
2251 );
2252
2253 let pb = ProgressBar::new(existing_binary_files.len() as u64);
2255 pb.set_style(
2256 ProgressStyle::default_bar()
2257 .template("š Fast loading [{elapsed_precise}] [{bar:40.green/cyan}] {pos}/{len} {msg}")?
2258 .progress_chars("āāā")
2259 );
2260
2261 for (i, file_path) in existing_binary_files.iter().enumerate() {
2262 pb.set_position(i as u64);
2263 pb.set_message("Processing...".to_string());
2264
2265 if engine.load_training_data_binary(file_path).is_ok() {
2266 loaded_count += 1;
2267 }
2268 }
2269
2270 pb.set_position(existing_binary_files.len() as u64);
2271 pb.finish_with_message(format!("ā
Loaded {loaded_count} binary files"));
2272 } else {
2273 println!("š¦ No binary files found, falling back to JSON auto-loading...");
2274 let _ = engine.auto_load_training_data()?;
2275 }
2276
2277
2278 let stats = engine.training_stats();
2279 println!(
2280 "ā” Fast engine ready with {} positions ({} binary files loaded)",
2281 stats.total_positions, loaded_count
2282 );
2283
2284 Ok(engine)
2285 }
2286
2287 pub fn new_with_auto_discovery(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
2290 println!("š Initializing engine with AUTO-DISCOVERY and format consolidation...");
2291 let mut engine = Self::new(vector_size);
2292 engine.enable_opening_book();
2293
2294 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
2296 println!("Loading complete");
2297 }
2298
2299 let discovered_files = AutoDiscovery::discover_training_files(".", true)?;
2301
2302 if discovered_files.is_empty() {
2303 println!("ā¹ļø No training data found. Use convert methods to create optimized files.");
2304 return Ok(engine);
2305 }
2306
2307 let consolidated = AutoDiscovery::consolidate_by_base_name(discovered_files.clone());
2309
2310 let mut total_loaded = 0;
2311 for (base_name, best_file) in &consolidated {
2312 println!("š Loading {} ({})", base_name, best_file.format);
2313
2314 let initial_size = engine.knowledge_base_size();
2315 engine.load_file_by_format(&best_file.path, &best_file.format)?;
2316 let loaded_count = engine.knowledge_base_size() - initial_size;
2317 total_loaded += loaded_count;
2318
2319 println!(" ā
Loaded {loaded_count} positions");
2320 }
2321
2322 let cleanup_candidates = AutoDiscovery::get_cleanup_candidates(&discovered_files);
2324 if !cleanup_candidates.is_empty() {
2325 println!(
2326 "š§¹ Found {} old format files that can be cleaned up:",
2327 cleanup_candidates.len()
2328 );
2329 AutoDiscovery::cleanup_old_formats(&cleanup_candidates, true)?; println!(" š” To actually remove old files, run: cargo run --bin cleanup_formats");
2332 }
2333
2334
2335 println!(
2336 "šÆ Engine ready: {} positions loaded from {} datasets",
2337 total_loaded,
2338 consolidated.len()
2339 );
2340 Ok(engine)
2341 }
2342
2343 pub fn new_with_instant_load(vector_size: usize) -> Result<Self, Box<dyn std::error::Error>> {
2346 println!("š Initializing engine with INSTANT loading...");
2347 let mut engine = Self::new(vector_size);
2348 engine.enable_opening_book();
2349
2350 if let Err(_e) = engine.enable_persistence("chess_vector_engine.db") {
2352 println!("Loading complete");
2353 }
2354
2355 let discovered_files = AutoDiscovery::discover_training_files(".", false)?;
2357
2358 if discovered_files.is_empty() {
2359 println!("ā¹ļø No user training data found, loading starter dataset...");
2361 if let Err(_e) = engine.load_starter_dataset() {
2362 println!("Loading complete");
2363 println!("ā¹ļø Starting with empty engine");
2364 } else {
2365 println!(
2366 "ā
Loaded starter dataset with {} positions",
2367 engine.knowledge_base_size()
2368 );
2369 }
2370 return Ok(engine);
2371 }
2372
2373 if let Some(best_file) = discovered_files.first() {
2375 println!(
2376 "ā” Loading {} format: {}",
2377 best_file.format,
2378 best_file.path.display()
2379 );
2380 engine.load_file_by_format(&best_file.path, &best_file.format)?;
2381 println!(
2382 "ā
Loaded {} positions from {} format",
2383 engine.knowledge_base_size(),
2384 best_file.format
2385 );
2386 }
2387
2388
2389 println!(
2390 "šÆ Engine ready: {} positions loaded",
2391 engine.knowledge_base_size()
2392 );
2393 Ok(engine)
2394 }
2395
2396 fn is_position_safe(&self, board: &Board) -> bool {
2401 match std::panic::catch_unwind(|| {
2403 use chess::MoveGen;
2404 let _legal_moves: Vec<ChessMove> = MoveGen::new_legal(board).collect();
2405 true
2406 }) {
2407 Ok(_) => true,
2408 Err(_) => {
2409 false
2411 }
2412 }
2413 }
2414
2415 pub fn check_gpu_acceleration(&self) -> Result<(), Box<dyn std::error::Error>> {
2417 match crate::gpu_acceleration::GPUAccelerator::new() {
2419 Ok(_) => {
2420 println!("š„ GPU acceleration available and ready");
2421 Ok(())
2422 }
2423 Err(_e) => Err("Processing...".to_string().into()),
2424 }
2425 }
2426
2427 pub fn load_starter_dataset(&mut self) -> Result<(), Box<dyn std::error::Error>> {
2429 let starter_data = if let Ok(file_content) =
2431 std::fs::read_to_string("training_data/starter_dataset.json")
2432 {
2433 file_content
2434 } else {
2435 r#"[
2437 {
2438 "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
2439 "evaluation": 0.0,
2440 "best_move": null,
2441 "depth": 0
2442 },
2443 {
2444 "fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
2445 "evaluation": 0.1,
2446 "best_move": "e7e5",
2447 "depth": 2
2448 },
2449 {
2450 "fen": "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
2451 "evaluation": 0.0,
2452 "best_move": "g1f3",
2453 "depth": 2
2454 }
2455 ]"#
2456 .to_string()
2457 };
2458
2459 let training_data: Vec<serde_json::Value> = serde_json::from_str(&starter_data)?;
2460
2461 for entry in training_data {
2462 if let (Some(fen), Some(evaluation)) = (entry.get("fen"), entry.get("evaluation")) {
2463 if let (Some(fen_str), Some(eval_f64)) = (fen.as_str(), evaluation.as_f64()) {
2464 match chess::Board::from_str(fen_str) {
2465 Ok(board) => {
2466 let mut eval = eval_f64 as f32;
2468
2469 if eval.abs() > 15.0 {
2472 eval /= 100.0;
2473 }
2474
2475 self.add_position(&board, eval);
2476 }
2477 Err(_) => {
2478 continue;
2480 }
2481 }
2482 }
2483 }
2484 }
2485
2486 Ok(())
2487 }
2488
2489 fn load_file_by_format(
2491 &mut self,
2492 path: &std::path::Path,
2493 format: &str,
2494 ) -> Result<(), Box<dyn std::error::Error>> {
2495 let file_size = std::fs::metadata(path)?.len();
2497
2498 if file_size > 10_000_000 {
2500 println!(
2501 "š Large file detected ({:.1} MB) - using ultra-fast loader",
2502 file_size as f64 / 1_000_000.0
2503 );
2504 return self.ultra_fast_load_any_format(path);
2505 }
2506
2507 match format {
2509 "MMAP" => self.load_training_data_mmap(path),
2510 "MSGPACK" => self.load_training_data_msgpack(path),
2511 "BINARY" => self.load_training_data_streaming_binary(path),
2512 "ZSTD" => self.load_training_data_compressed(path),
2513 "JSON" => self.load_training_data_streaming_json_v2(path),
2514 _ => Err("Processing...".to_string().into()),
2515 }
2516 }
2517
2518 pub fn ultra_fast_load_any_format<P: AsRef<std::path::Path>>(
2520 &mut self,
2521 path: P,
2522 ) -> Result<(), Box<dyn std::error::Error>> {
2523 let mut loader = UltraFastLoader::new_for_massive_datasets();
2524 loader.ultra_load_binary(path, self)?;
2525
2526 let stats = loader.get_stats();
2527 println!("š Ultra-fast loading complete:");
2528 println!(" ā
Loaded: {} positions", stats.loaded);
2529 println!("Loading complete");
2530 println!("Loading complete");
2531 println!(" š Success rate: {:.1}%", stats.success_rate() * 100.0);
2532
2533 Ok(())
2534 }
2535
2536 pub fn load_training_data_streaming_binary<P: AsRef<std::path::Path>>(
2539 &mut self,
2540 path: P,
2541 ) -> Result<(), Box<dyn std::error::Error>> {
2542 let mut loader = StreamingLoader::new();
2543 loader.stream_load_binary(path, self)?;
2544
2545 println!("š Streaming binary load complete:");
2546 println!(" Loaded: {} new positions", loader.loaded_count);
2547 println!("Loading complete");
2548 println!("Loading complete");
2549
2550 Ok(())
2551 }
2552
2553 pub fn load_training_data_streaming_json_v2<P: AsRef<std::path::Path>>(
2556 &mut self,
2557 path: P,
2558 ) -> Result<(), Box<dyn std::error::Error>> {
2559 let mut loader = StreamingLoader::new();
2560
2561 let batch_size = if std::fs::metadata(path.as_ref())?.len() > 100_000_000 {
2563 20000 } else {
2566 5000 };
2568
2569 loader.stream_load_json(path, self, batch_size)?;
2570
2571 println!("š Streaming JSON load complete:");
2572 println!(" Loaded: {} new positions", loader.loaded_count);
2573 println!("Loading complete");
2574 println!("Loading complete");
2575
2576 Ok(())
2577 }
2578
2579 pub fn new_for_massive_datasets(
2582 vector_size: usize,
2583 ) -> Result<Self, Box<dyn std::error::Error>> {
2584 println!("š Initializing engine for MASSIVE datasets (100k-1M+ positions)...");
2585 let mut engine = Self::new(vector_size);
2586 engine.enable_opening_book();
2587
2588 let discovered_files = AutoDiscovery::discover_training_files(".", false)?;
2590
2591 if discovered_files.is_empty() {
2592 println!("ā¹ļø No training data found");
2593 return Ok(engine);
2594 }
2595
2596 let largest_file = discovered_files
2598 .iter()
2599 .max_by_key(|f| f.size_bytes)
2600 .unwrap();
2601
2602 println!(
2603 "šÆ Loading largest dataset: {} ({} bytes)",
2604 largest_file.path.display(),
2605 largest_file.size_bytes
2606 );
2607
2608 engine.ultra_fast_load_any_format(&largest_file.path)?;
2610
2611 println!(
2612 "šÆ Engine ready: {} positions loaded",
2613 engine.knowledge_base_size()
2614 );
2615 Ok(engine)
2616 }
2617
2618 pub fn convert_to_msgpack() -> Result<(), Box<dyn std::error::Error>> {
2621 use serde_json::Value;
2622 use std::fs::File;
2623 use std::io::{BufReader, BufWriter};
2624
2625 if std::path::Path::new("training_data_a100.bin").exists() {
2627 Self::convert_a100_binary_to_json()?;
2628 }
2629
2630 let input_files = [
2631 "training_data.json",
2632 "tactical_training_data.json",
2633 "training_data_a100.json",
2634 ];
2635
2636 for input_file in &input_files {
2637 let input_path = std::path::Path::new(input_file);
2638 if !input_path.exists() {
2639 continue;
2640 }
2641
2642 let output_file_path = input_file.replace(".json", ".msgpack");
2643 println!("š Converting {input_file} ā {output_file_path} (MessagePack format)");
2644
2645 let file = File::open(input_path)?;
2647 let reader = BufReader::new(file);
2648 let json_value: Value = serde_json::from_reader(reader)?;
2649
2650 let data: Vec<(String, f32)> = match json_value {
2651 Value::Array(arr) if !arr.is_empty() => {
2653 if let Some(first) = arr.first() {
2654 if first.is_array() {
2655 arr.into_iter()
2657 .filter_map(|item| {
2658 if let Value::Array(tuple) = item {
2659 if tuple.len() >= 2 {
2660 let fen = tuple[0].as_str()?.to_string();
2661 let mut eval = tuple[1].as_f64()? as f32;
2662
2663 if eval.abs() > 15.0 {
2667 eval /= 100.0;
2668 }
2669
2670 Some((fen, eval))
2671 } else {
2672 None
2673 }
2674 } else {
2675 None
2676 }
2677 })
2678 .collect()
2679 } else if first.is_object() {
2680 arr.into_iter()
2682 .filter_map(|item| {
2683 if let Value::Object(obj) = item {
2684 let fen = obj.get("fen")?.as_str()?.to_string();
2685 let mut eval = obj.get("evaluation")?.as_f64()? as f32;
2686
2687 if eval.abs() > 15.0 {
2691 eval /= 100.0;
2692 }
2693
2694 Some((fen, eval))
2695 } else {
2696 None
2697 }
2698 })
2699 .collect()
2700 } else {
2701 return Err("Processing...".to_string().into());
2702 }
2703 } else {
2704 Vec::new()
2705 }
2706 }
2707 _ => return Err("Processing...".to_string().into()),
2708 };
2709
2710 if data.is_empty() {
2711 println!("Loading complete");
2712 continue;
2713 }
2714
2715 let output_file = File::create(&output_file_path)?;
2717 let mut writer = BufWriter::new(output_file);
2718 rmp_serde::encode::write(&mut writer, &data)?;
2719
2720 let input_size = input_path.metadata()?.len();
2721 let output_size = std::path::Path::new(&output_file_path).metadata()?.len();
2722 let ratio = input_size as f64 / output_size as f64;
2723
2724 println!(
2725 "ā
Converted: {} ā {} ({:.1}x size reduction, {} positions)",
2726 Self::format_bytes(input_size),
2727 Self::format_bytes(output_size),
2728 ratio,
2729 data.len()
2730 );
2731 }
2732
2733 Ok(())
2734 }
2735
2736 pub fn convert_a100_binary_to_json() -> Result<(), Box<dyn std::error::Error>> {
2738 use std::fs::File;
2739 use std::io::BufWriter;
2740
2741 let binary_path = "training_data_a100.bin";
2742 let json_path = "training_data_a100.json";
2743
2744 if !std::path::Path::new(binary_path).exists() {
2745 println!("Loading complete");
2746 return Ok(());
2747 }
2748
2749 println!("š Converting A100 binary data {binary_path} ā {json_path} (JSON format)");
2750
2751 let mut engine = ChessVectorEngine::new(1024);
2753 engine.load_training_data_binary(binary_path)?;
2754
2755 let mut data = Vec::new();
2757 for (i, board) in engine.position_boards.iter().enumerate() {
2758 if i < engine.position_evaluations.len() {
2759 data.push(serde_json::json!({
2760 "fen": board.to_string(),
2761 "evaluation": engine.position_evaluations[i],
2762 "depth": 15,
2763 "game_id": i
2764 }));
2765 }
2766 }
2767
2768 let file = File::create(json_path)?;
2770 let writer = BufWriter::new(file);
2771 serde_json::to_writer(writer, &data)?;
2772
2773 println!(
2774 "ā
Converted A100 data: {} positions ā {}",
2775 data.len(),
2776 json_path
2777 );
2778 Ok(())
2779 }
2780
2781 pub fn convert_to_zstd() -> Result<(), Box<dyn std::error::Error>> {
2784 use std::fs::File;
2785 use std::io::{BufReader, BufWriter};
2786
2787 if std::path::Path::new("training_data_a100.bin").exists() {
2789 Self::convert_a100_binary_to_json()?;
2790 }
2791
2792 let input_files = [
2793 ("training_data.json", "training_data.zst"),
2794 ("tactical_training_data.json", "tactical_training_data.zst"),
2795 ("training_data_a100.json", "training_data_a100.zst"),
2796 ("training_data.bin", "training_data.bin.zst"),
2797 (
2798 "tactical_training_data.bin",
2799 "tactical_training_data.bin.zst",
2800 ),
2801 ("training_data_a100.bin", "training_data_a100.bin.zst"),
2802 ];
2803
2804 for (input_file, output_file) in &input_files {
2805 let input_path = std::path::Path::new(input_file);
2806 if !input_path.exists() {
2807 continue;
2808 }
2809
2810 println!("š Converting {input_file} ā {output_file} (Zstd compression)");
2811
2812 let input_file = File::open(input_path)?;
2813 let output_file_handle = File::create(output_file)?;
2814 let writer = BufWriter::new(output_file_handle);
2815 let mut encoder = zstd::stream::Encoder::new(writer, 9)?; std::io::copy(&mut BufReader::new(input_file), &mut encoder)?;
2818 encoder.finish()?;
2819
2820 let input_size = input_path.metadata()?.len();
2821 let output_size = std::path::Path::new(output_file).metadata()?.len();
2822 let ratio = input_size as f64 / output_size as f64;
2823
2824 println!(
2825 "ā
Compressed: {} ā {} ({:.1}x size reduction)",
2826 Self::format_bytes(input_size),
2827 Self::format_bytes(output_size),
2828 ratio
2829 );
2830 }
2831
2832 Ok(())
2833 }
2834
2835 pub fn convert_to_mmap() -> Result<(), Box<dyn std::error::Error>> {
2838 use std::fs::File;
2839 use std::io::{BufReader, BufWriter};
2840
2841 if std::path::Path::new("training_data_a100.bin").exists() {
2843 Self::convert_a100_binary_to_json()?;
2844 }
2845
2846 let input_files = [
2847 ("training_data.json", "training_data.mmap"),
2848 ("tactical_training_data.json", "tactical_training_data.mmap"),
2849 ("training_data_a100.json", "training_data_a100.mmap"),
2850 ("training_data.msgpack", "training_data.mmap"),
2851 (
2852 "tactical_training_data.msgpack",
2853 "tactical_training_data.mmap",
2854 ),
2855 ("training_data_a100.msgpack", "training_data_a100.mmap"),
2856 ];
2857
2858 for (input_file, output_file) in &input_files {
2859 let input_path = std::path::Path::new(input_file);
2860 if !input_path.exists() {
2861 continue;
2862 }
2863
2864 println!("š Converting {input_file} ā {output_file} (Memory-mapped format)");
2865
2866 let data: Vec<(String, f32)> = if input_file.ends_with(".json") {
2868 let file = File::open(input_path)?;
2869 let reader = BufReader::new(file);
2870 let json_value: Value = serde_json::from_reader(reader)?;
2871
2872 match json_value {
2873 Value::Array(arr) if !arr.is_empty() => {
2875 if let Some(first) = arr.first() {
2876 if first.is_array() {
2877 arr.into_iter()
2879 .filter_map(|item| {
2880 if let Value::Array(tuple) = item {
2881 if tuple.len() >= 2 {
2882 let fen = tuple[0].as_str()?.to_string();
2883 let mut eval = tuple[1].as_f64()? as f32;
2884
2885 if eval.abs() > 15.0 {
2889 eval /= 100.0;
2890 }
2891
2892 Some((fen, eval))
2893 } else {
2894 None
2895 }
2896 } else {
2897 None
2898 }
2899 })
2900 .collect()
2901 } else if first.is_object() {
2902 arr.into_iter()
2904 .filter_map(|item| {
2905 if let Value::Object(obj) = item {
2906 let fen = obj.get("fen")?.as_str()?.to_string();
2907 let mut eval = obj.get("evaluation")?.as_f64()? as f32;
2908
2909 if eval.abs() > 15.0 {
2913 eval /= 100.0;
2914 }
2915
2916 Some((fen, eval))
2917 } else {
2918 None
2919 }
2920 })
2921 .collect()
2922 } else {
2923 return Err("Failed to process training data".into());
2924 }
2925 } else {
2926 Vec::new()
2927 }
2928 }
2929 _ => return Err("Processing...".to_string().into()),
2930 }
2931 } else if input_file.ends_with(".msgpack") {
2932 let file = File::open(input_path)?;
2933 let reader = BufReader::new(file);
2934 rmp_serde::from_read(reader)?
2935 } else {
2936 return Err("Unsupported input format for memory mapping".into());
2937 };
2938
2939 let output_file_handle = File::create(output_file)?;
2941 let mut writer = BufWriter::new(output_file_handle);
2942 rmp_serde::encode::write(&mut writer, &data)?;
2943
2944 let input_size = input_path.metadata()?.len();
2945 let output_size = std::path::Path::new(output_file).metadata()?.len();
2946
2947 println!(
2948 "ā
Memory-mapped file created: {} ā {} ({} positions)",
2949 Self::format_bytes(input_size),
2950 Self::format_bytes(output_size),
2951 data.len()
2952 );
2953 }
2954
2955 Ok(())
2956 }
2957
2958 pub fn convert_json_to_binary() -> Result<Vec<String>, Box<dyn std::error::Error>> {
2960 use indicatif::{ProgressBar, ProgressStyle};
2961
2962 let json_files = [
2963 "training_data.json",
2964 "tactical_training_data.json",
2965 "engine_training.json",
2966 "chess_training.json",
2967 ];
2968
2969 let existing_json_files: Vec<_> = json_files
2971 .iter()
2972 .filter(|&file_path| std::path::Path::new(file_path).exists())
2973 .collect();
2974
2975 if existing_json_files.is_empty() {
2976 println!("ā¹ļø No JSON training files found to convert");
2977 return Ok(Vec::new());
2978 }
2979
2980 println!(
2981 "š Converting {} JSON files to binary format...",
2982 existing_json_files.len()
2983 );
2984
2985 let pb = ProgressBar::new(existing_json_files.len() as u64);
2987 pb.set_style(
2988 ProgressStyle::default_bar()
2989 .template(
2990 "š¦ Converting [{elapsed_precise}] [{bar:40.yellow/blue}] {pos}/{len} {msg}",
2991 )?
2992 .progress_chars("āāā"),
2993 );
2994
2995 let mut converted_files = Vec::new();
2996
2997 for (i, json_file) in existing_json_files.iter().enumerate() {
2998 pb.set_position(i as u64);
2999 pb.set_message("Processing...".to_string());
3000
3001 let binary_file = std::path::Path::new(json_file).with_extension("bin");
3002
3003 let mut temp_engine = Self::new(1024);
3005 if temp_engine
3006 .load_training_data_incremental(json_file)
3007 .is_ok()
3008 {
3009 if temp_engine.save_training_data_binary(&binary_file).is_ok() {
3010 converted_files.push(binary_file.to_string_lossy().to_string());
3011 println!("ā
Converted {json_file} to binary format");
3012 } else {
3013 println!("Loading complete");
3014 }
3015 } else {
3016 println!("Loading complete");
3017 }
3018 }
3019
3020 pb.set_position(existing_json_files.len() as u64);
3021 pb.finish_with_message(format!("ā
Converted {} files", converted_files.len()));
3022
3023 if !converted_files.is_empty() {
3024 println!("š Binary conversion complete! Startup will be 5-15x faster next time.");
3025 println!("š Conversion summary:");
3026 for _conversion in &converted_files {
3027 println!("Loading complete");
3028 }
3029 }
3030
3031 Ok(converted_files)
3032 }
3033
3034 pub fn is_lsh_enabled(&self) -> bool {
3036 self.use_lsh
3037 }
3038
3039 pub fn lsh_stats(&self) -> Option<crate::lsh::LSHStats> {
3041 self.lsh_index.as_ref().map(|lsh| lsh.stats())
3042 }
3043
3044
3045
3046 pub fn enable_opening_book(&mut self) {
3048 self.opening_book = Some(OpeningBook::with_standard_openings());
3049 }
3050
3051 pub fn set_opening_book(&mut self, book: OpeningBook) {
3053 self.opening_book = Some(book);
3054 }
3055
3056 pub fn is_opening_position(&self, board: &Board) -> bool {
3058 self.opening_book
3059 .as_ref()
3060 .map(|book| book.contains(board))
3061 .unwrap_or(false)
3062 }
3063
3064 pub fn get_opening_entry(&self, board: &Board) -> Option<&OpeningEntry> {
3066 self.opening_book.as_ref()?.lookup(board)
3067 }
3068
3069 pub fn opening_book_stats(&self) -> Option<OpeningBookStats> {
3071 self.opening_book.as_ref().map(|book| book.get_statistics())
3072 }
3073
3074 pub fn add_position_with_move(
3076 &mut self,
3077 board: &Board,
3078 evaluation: f32,
3079 chess_move: Option<ChessMove>,
3080 move_outcome: Option<f32>,
3081 ) {
3082 let position_index = self.knowledge_base_size();
3083
3084 self.add_position(board, evaluation);
3086
3087 if let (Some(mov), Some(outcome)) = (chess_move, move_outcome) {
3089 self.position_moves
3090 .entry(position_index)
3091 .or_default()
3092 .push((mov, outcome));
3093 }
3094 }
3095
3096 pub fn recommend_moves_with_tactical_search(
3098 &mut self,
3099 board: &Board,
3100 num_recommendations: usize,
3101 ) -> Vec<MoveRecommendation> {
3102 use chess::MoveGen;
3104 let legal_moves: Vec<ChessMove> = MoveGen::new_legal(board).collect();
3105
3106 if legal_moves.is_empty() {
3107 return Vec::new();
3108 }
3109
3110 let mut move_evaluations = Vec::new();
3111
3112 for chess_move in legal_moves.iter().take(20) {
3113 let temp_board = board.make_move_new(*chess_move);
3115
3116 let evaluation = if let Some(ref mut tactical_search) = self.tactical_search {
3118 let result = tactical_search.search(&temp_board);
3119 result.evaluation
3120 } else {
3121 self.evaluate_position(&temp_board).unwrap_or(0.0)
3123 };
3124
3125 let normalized_eval = if board.side_to_move() == chess::Color::White {
3126 evaluation
3127 } else {
3128 -evaluation
3129 };
3130
3131 move_evaluations.push((*chess_move, normalized_eval));
3132 }
3133
3134 move_evaluations.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
3136
3137 let mut recommendations = Vec::new();
3139 for (i, (chess_move, evaluation)) in move_evaluations
3140 .iter()
3141 .enumerate()
3142 .take(num_recommendations)
3143 {
3144 let confidence = if i == 0 { 0.8 } else { 0.6 - (i as f32 * 0.1) }; recommendations.push(MoveRecommendation {
3146 chess_move: *chess_move,
3147 confidence: confidence.max(0.3),
3148 from_similar_position_count: 0,
3149 average_outcome: *evaluation,
3150 });
3151 }
3152
3153 recommendations
3154 }
3155
3156 pub fn is_move_tactically_safe(&mut self, board: &Board, chess_move: chess::ChessMove) -> bool {
3158 let new_board = board.make_move_new(chess_move);
3160
3161 if *new_board.checkers() != chess::EMPTY && new_board.side_to_move() != board.side_to_move()
3163 {
3164 return false; }
3166
3167 let original_material = self.calculate_material_balance(board);
3169 let new_material = self.calculate_material_balance(&new_board);
3170
3171 let immediate_material_change = if board.side_to_move() == chess::Color::White {
3172 new_material.white_material - original_material.white_material
3173 } else {
3174 new_material.black_material - original_material.black_material
3175 };
3176
3177 if immediate_material_change <= -0.8 || self.is_tactically_critical_position(&new_board) {
3179 if let Some(ref mut tactical_search) = self.tactical_search {
3180 let original_config = tactical_search.config.clone();
3182 tactical_search.config.max_depth = 10; tactical_search.config.max_time_ms = 500; tactical_search.config.enable_quiescence = true; let tactical_result = tactical_search.search(&new_board);
3188
3189 tactical_search.config = original_config;
3191
3192 let position_evaluation = -tactical_result.evaluation;
3194
3195 if position_evaluation < -1.5 {
3197 return false;
3198 }
3199
3200 if immediate_material_change <= -2.0 {
3202 if position_evaluation < -0.5 {
3204 return false; }
3206 }
3207 } else {
3208 return immediate_material_change >= -0.5;
3210 }
3211 }
3212
3213 if self.has_hanging_pieces(&new_board) {
3215 return false;
3216 }
3217
3218 let legal_moves = chess::MoveGen::new_legal(&new_board);
3220 for opponent_move in legal_moves {
3221 if let Some(captured_piece) = new_board.piece_on(opponent_move.get_dest()) {
3222 if captured_piece == chess::Piece::King {
3223 return false; }
3225 }
3226 }
3227
3228 true }
3230
3231 fn is_tactically_critical_position(&self, board: &Board) -> bool {
3233 if *board.checkers() != chess::EMPTY {
3235 return true; }
3237
3238 let attack_count = self.count_attacked_pieces(board);
3240 if attack_count > 2 {
3241 return true; }
3243
3244 if self.has_tactical_motifs(board) {
3246 return true;
3247 }
3248
3249 if self.is_king_exposed(board) {
3251 return true;
3252 }
3253
3254 false
3255 }
3256
3257 fn has_hanging_pieces(&self, board: &Board) -> bool {
3259 for square in chess::ALL_SQUARES {
3260 if let Some(piece) = board.piece_on(square) {
3261 if board.color_on(square) == Some(board.side_to_move()) {
3262 if self.is_piece_hanging(board, square, piece) {
3264 return true;
3265 }
3266 }
3267 }
3268 }
3269 false
3270 }
3271
3272 fn is_piece_hanging(&self, board: &Board, square: chess::Square, piece: chess::Piece) -> bool {
3274 if piece == chess::Piece::Pawn || piece == chess::Piece::King {
3276 return false;
3277 }
3278
3279 if !self.is_square_attacked(board, square, !board.side_to_move()) {
3281 return false; }
3283
3284 if self.is_square_defended(board, square, board.side_to_move()) {
3286 return false; }
3288
3289 true }
3291
3292 fn is_square_defended(
3294 &self,
3295 board: &Board,
3296 square: chess::Square,
3297 by_color: chess::Color,
3298 ) -> bool {
3299 let friendly_pieces = board.color_combined(by_color);
3302
3303 for rank_offset in -2..=2 {
3305 for file_offset in -2..=2 {
3306 if rank_offset == 0 && file_offset == 0 {
3307 continue;
3308 }
3309
3310 let current_rank = square.get_rank().to_index() as i8;
3311 let current_file = square.get_file().to_index() as i8;
3312
3313 let new_rank = current_rank + rank_offset;
3314 let new_file = current_file + file_offset;
3315
3316 if new_rank >= 0 && new_rank < 8 && new_file >= 0 && new_file < 8 {
3317 let check_square = chess::Square::make_square(
3318 chess::Rank::from_index(new_rank as usize),
3319 chess::File::from_index(new_file as usize),
3320 );
3321
3322 if (friendly_pieces & chess::BitBoard::from_square(check_square))
3323 != chess::EMPTY
3324 {
3325 return true; }
3327 }
3328 }
3329 }
3330
3331 false
3332 }
3333
3334 fn count_attacked_pieces(&self, board: &Board) -> u32 {
3336 let mut count = 0;
3337 for square in chess::ALL_SQUARES {
3338 if let Some(_piece) = board.piece_on(square) {
3339 if board.color_on(square) == Some(board.side_to_move()) {
3340 if self.is_square_attacked(board, square, !board.side_to_move()) {
3341 count += 1;
3342 }
3343 }
3344 }
3345 }
3346 count
3347 }
3348
3349 fn has_tactical_motifs(&self, board: &Board) -> bool {
3351 for square in chess::ALL_SQUARES {
3354 if let Some(piece) = board.piece_on(square) {
3355 if board.color_on(square) == Some(!board.side_to_move()) {
3356 match piece {
3358 chess::Piece::Queen | chess::Piece::Rook | chess::Piece::Bishop => {
3359 if self.creates_tactical_threat(board, square, piece) {
3361 return true;
3362 }
3363 }
3364 chess::Piece::Knight => {
3365 if self.creates_fork_threat(board, square) {
3367 return true;
3368 }
3369 }
3370 _ => {}
3371 }
3372 }
3373 }
3374 }
3375 false
3376 }
3377
3378 fn creates_tactical_threat(
3380 &self,
3381 board: &Board,
3382 square: chess::Square,
3383 piece: chess::Piece,
3384 ) -> bool {
3385 let mut threatened_pieces = 0;
3387 for target_square in chess::ALL_SQUARES {
3388 if let Some(_target_piece) = board.piece_on(target_square) {
3389 if board.color_on(target_square) == Some(board.side_to_move()) {
3390 if self.piece_attacks_square(board, square, piece, target_square) {
3392 threatened_pieces += 1;
3393 }
3394 }
3395 }
3396 }
3397 threatened_pieces >= 2 }
3399
3400 fn creates_fork_threat(&self, board: &Board, knight_square: chess::Square) -> bool {
3402 let mut attacked_valuable_pieces = 0;
3404 let knight_attacks = self.get_knight_attacks(knight_square);
3405
3406 for target_square in chess::ALL_SQUARES {
3407 if (knight_attacks & chess::BitBoard::from_square(target_square)) != chess::EMPTY {
3408 if let Some(piece) = board.piece_on(target_square) {
3409 if board.color_on(target_square) == Some(board.side_to_move()) {
3410 match piece {
3412 chess::Piece::Queen | chess::Piece::Rook | chess::Piece::King => {
3413 attacked_valuable_pieces += 1;
3414 }
3415 _ => {}
3416 }
3417 }
3418 }
3419 }
3420 }
3421
3422 attacked_valuable_pieces >= 2
3423 }
3424
3425 fn get_knight_attacks(&self, square: chess::Square) -> chess::BitBoard {
3427 let mut attacks = chess::EMPTY;
3429 let rank = square.get_rank().to_index() as i8;
3430 let file = square.get_file().to_index() as i8;
3431
3432 let moves = [
3433 (2, 1),
3434 (2, -1),
3435 (-2, 1),
3436 (-2, -1),
3437 (1, 2),
3438 (1, -2),
3439 (-1, 2),
3440 (-1, -2),
3441 ];
3442
3443 for (rank_offset, file_offset) in moves {
3444 let new_rank = rank + rank_offset;
3445 let new_file = file + file_offset;
3446
3447 if new_rank >= 0 && new_rank < 8 && new_file >= 0 && new_file < 8 {
3448 let target_square = chess::Square::make_square(
3449 chess::Rank::from_index(new_rank as usize),
3450 chess::File::from_index(new_file as usize),
3451 );
3452 attacks |= chess::BitBoard::from_square(target_square);
3453 }
3454 }
3455
3456 attacks
3457 }
3458
3459 fn piece_attacks_square(
3461 &self,
3462 _board: &Board,
3463 piece_square: chess::Square,
3464 piece: chess::Piece,
3465 target_square: chess::Square,
3466 ) -> bool {
3467 match piece {
3468 chess::Piece::Knight => {
3469 let knight_attacks = self.get_knight_attacks(piece_square);
3470 (knight_attacks & chess::BitBoard::from_square(target_square)) != chess::EMPTY
3471 }
3472 chess::Piece::Queen | chess::Piece::Rook | chess::Piece::Bishop => {
3473 let piece_rank = piece_square.get_rank().to_index();
3475 let piece_file = piece_square.get_file().to_index();
3476 let target_rank = target_square.get_rank().to_index();
3477 let target_file = target_square.get_file().to_index();
3478
3479 if (piece == chess::Piece::Rook || piece == chess::Piece::Queen)
3481 && (piece_rank == target_rank || piece_file == target_file)
3482 {
3483 return true;
3484 }
3485
3486 if (piece == chess::Piece::Bishop || piece == chess::Piece::Queen)
3488 && ((piece_rank as i8 - target_rank as i8).abs()
3489 == (piece_file as i8 - target_file as i8).abs())
3490 {
3491 return true;
3492 }
3493
3494 false
3495 }
3496 _ => false,
3497 }
3498 }
3499
3500 fn has_king_danger(&self, board: &Board) -> bool {
3503 for color in [chess::Color::White, chess::Color::Black] {
3504 let king_square = board.king_square(color);
3505
3506 let mut attackers = 0;
3508 let opponent_color = !color;
3509
3510 for piece_type in [
3512 chess::Piece::Queen,
3513 chess::Piece::Rook,
3514 chess::Piece::Bishop,
3515 chess::Piece::Knight,
3516 chess::Piece::Pawn,
3517 ] {
3518 let opponent_pieces =
3519 board.pieces(piece_type) & board.color_combined(opponent_color);
3520 for piece_square in opponent_pieces {
3521 if self.piece_attacks_square(board, piece_square, piece_type, king_square) {
3522 attackers += 1;
3523 if attackers >= 2 {
3524 return true; }
3526 }
3527 }
3528 }
3529
3530 let king_file = king_square.get_file().to_index();
3532 let king_rank = king_square.get_rank().to_index();
3533 if king_file >= 2 && king_file <= 5 && king_rank >= 2 && king_rank <= 5 {
3534 return true; }
3536 }
3537
3538 false
3539 }
3540
3541 fn is_king_exposed(&self, board: &Board) -> bool {
3542 let king_square = board.king_square(board.side_to_move());
3543
3544 let mut escape_squares = 0;
3546 for rank_offset in -1..=1 {
3547 for file_offset in -1..=1 {
3548 if rank_offset == 0 && file_offset == 0 {
3549 continue;
3550 }
3551
3552 let king_rank = king_square.get_rank().to_index() as i8;
3553 let king_file = king_square.get_file().to_index() as i8;
3554
3555 let new_rank = king_rank + rank_offset;
3556 let new_file = king_file + file_offset;
3557
3558 if new_rank >= 0 && new_rank < 8 && new_file >= 0 && new_file < 8 {
3559 let escape_square = chess::Square::make_square(
3560 chess::Rank::from_index(new_rank as usize),
3561 chess::File::from_index(new_file as usize),
3562 );
3563
3564 if board.piece_on(escape_square).is_none()
3566 && !self.is_square_attacked(board, escape_square, !board.side_to_move())
3567 {
3568 escape_squares += 1;
3569 }
3570 }
3571 }
3572 }
3573
3574 escape_squares < 3 }
3576
3577 fn calculate_position_criticality(
3579 &self,
3580 board: &Board,
3581 material_deficit: f32,
3582 position_complexity: f32,
3583 ) -> f32 {
3584 let mut criticality = 0.0;
3585
3586 criticality += (material_deficit / 10.0).min(0.4); criticality += position_complexity * 0.3; if *board.checkers() != chess::EMPTY {
3594 criticality += 0.4;
3595 }
3596
3597 let attacked_pieces = self.count_attacked_pieces(board);
3599 criticality += (attacked_pieces as f32 * 0.05).min(0.2); if self.has_tactical_motifs(board) {
3603 criticality += 0.3;
3604 }
3605
3606 if self.is_king_exposed(board) {
3608 criticality += 0.2;
3609 }
3610
3611 if self.has_hanging_pieces(board) {
3613 criticality += 0.4;
3614 }
3615
3616 criticality.max(0.0).min(1.0)
3618 }
3619
3620 fn calculate_adaptive_search_parameters(
3622 &self,
3623 criticality: f32,
3624 material_deficit: f32,
3625 complexity: f32,
3626 ) -> (u32, u64) {
3627 let mut depth = 6u32;
3629 let mut time_ms = 1000u64;
3630
3631 if criticality > 0.9 {
3633 depth = 14;
3635 time_ms = 5000;
3636 } else if criticality > 0.7 {
3637 depth = 12;
3639 time_ms = 3000;
3640 } else if criticality > 0.5 {
3641 depth = 10;
3643 time_ms = 2000;
3644 } else if criticality > 0.3 {
3645 depth = 8;
3647 time_ms = 1500;
3648 }
3649
3650 if material_deficit > 3.0 {
3652 depth = depth.saturating_add(3);
3653 time_ms = (time_ms as f64 * 1.5) as u64;
3654 } else if material_deficit > 1.0 {
3655 depth = depth.saturating_add(1);
3656 time_ms = (time_ms as f64 * 1.2) as u64;
3657 }
3658
3659 if complexity > 0.7 {
3661 depth = depth.saturating_add(2);
3662 time_ms = (time_ms as f64 * 1.3) as u64;
3663 }
3664
3665 depth = depth.max(4).min(16);
3667 time_ms = time_ms.max(200).min(10000);
3668
3669 (depth, time_ms)
3670 }
3671
3672 pub fn recommend_moves(
3674 &mut self,
3675 board: &Board,
3676 num_recommendations: usize,
3677 ) -> Vec<MoveRecommendation> {
3678 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
3680 let proactive_moves = strategic_evaluator.generate_proactive_moves(board);
3681
3682 if !proactive_moves.is_empty() {
3683 let mut strategic_recommendations = Vec::new();
3684
3685 for (chess_move, strategic_value) in
3686 proactive_moves.iter().take(num_recommendations)
3687 {
3688 strategic_recommendations.push(MoveRecommendation {
3689 chess_move: *chess_move,
3690 confidence: (strategic_value / 80.0).clamp(0.3, 0.95), from_similar_position_count: 0, average_outcome: *strategic_value / 100.0, });
3694 }
3695
3696 if strategic_recommendations.len() >= num_recommendations {
3698 strategic_recommendations.truncate(num_recommendations);
3699 return strategic_recommendations;
3700 }
3701
3702 } else {
3705 return self.recommend_moves_with_tactical_search(board, num_recommendations);
3708 }
3709 }
3710
3711 if let Some(entry) = self.get_opening_entry(board) {
3725 let mut recommendations = Vec::new();
3726
3727 for (chess_move, strength) in &entry.best_moves {
3728 recommendations.push(MoveRecommendation {
3729 chess_move: *chess_move,
3730 confidence: strength * 0.9, from_similar_position_count: 1,
3732 average_outcome: entry.evaluation,
3733 });
3734 }
3735
3736 recommendations.sort_by(|a, b| {
3738 b.confidence
3739 .partial_cmp(&a.confidence)
3740 .unwrap_or(std::cmp::Ordering::Equal)
3741 });
3742 recommendations.truncate(num_recommendations);
3743 return recommendations;
3744 }
3745
3746 let similar_positions = self.find_similar_positions_with_indices(board, 20);
3748
3749 let mut move_data: HashMap<ChessMove, Vec<(f32, f32)>> = HashMap::new(); use chess::MoveGen;
3754 let legal_moves: Vec<ChessMove> = match std::panic::catch_unwind(|| {
3755 MoveGen::new_legal(board).collect::<Vec<ChessMove>>()
3756 }) {
3757 Ok(moves) => moves,
3758 Err(_) => {
3759 return Vec::new();
3761 }
3762 };
3763
3764 for (position_index, _eval, similarity) in similar_positions {
3766 if let Some(moves) = self.position_moves.get(&position_index) {
3767 for &(chess_move, outcome) in moves {
3768 if legal_moves.contains(&chess_move) {
3770 move_data
3771 .entry(chess_move)
3772 .or_default()
3773 .push((similarity, outcome));
3774 }
3775 }
3776 }
3777 }
3778
3779 if self.tactical_search.is_some() {
3781 if let Some(ref mut tactical_search) = self.tactical_search {
3782 let tactical_result =
3784 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
3785 if strategic_evaluator.should_play_aggressively(board) {
3787 tactical_search.search(board)
3789 } else {
3790 tactical_search.search(board)
3792 }
3793 } else {
3794 tactical_search.search(board)
3796 };
3797
3798 if let Some(best_move) = tactical_result.best_move {
3800 let mut temp_board = *board;
3802 temp_board = temp_board.make_move_new(best_move);
3803 let move_evaluation = tactical_search.search(&temp_board).evaluation;
3804
3805 let confidence = if let Some(ref strategic_evaluator) = self.strategic_evaluator
3807 {
3808 let strategic_eval = strategic_evaluator.evaluate_strategic(board);
3809 if strategic_eval.attacking_moves.contains(&best_move) {
3811 0.98 } else if strategic_eval.positional_moves.contains(&best_move) {
3813 0.95 } else {
3815 0.90 }
3817 } else {
3818 0.95 };
3820
3821 move_data.insert(best_move, vec![(confidence, move_evaluation)]);
3822 }
3823
3824 let mut ordered_moves = legal_moves.clone();
3827
3828 ordered_moves.sort_by(|a, b| {
3830 let a_is_capture = board.piece_on(a.get_dest()).is_some();
3831 let b_is_capture = board.piece_on(b.get_dest()).is_some();
3832
3833 match (a_is_capture, b_is_capture) {
3834 (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => {
3837 let a_centrality = move_centrality(a);
3839 let b_centrality = move_centrality(b);
3840 b_centrality
3841 .partial_cmp(&a_centrality)
3842 .unwrap_or(std::cmp::Ordering::Equal)
3843 }
3844 }
3845 });
3846
3847 for chess_move in ordered_moves.into_iter() {
3850 move_data.entry(chess_move).or_insert_with(|| {
3851 let mut temp_board = *board;
3853 temp_board = temp_board.make_move_new(chess_move);
3854 let move_evaluation = tactical_search.search(&temp_board).evaluation;
3855
3856 let confidence =
3858 if let Some(ref strategic_evaluator) = self.strategic_evaluator {
3859 let strategic_eval = strategic_evaluator.evaluate_strategic(board);
3860 if strategic_eval.attacking_moves.contains(&chess_move) {
3862 0.92 } else if strategic_eval.positional_moves.contains(&chess_move) {
3864 0.88 } else {
3866 0.85 }
3868 } else {
3869 0.90 };
3871
3872 vec![(confidence, move_evaluation)]
3873 });
3874 }
3875 } else {
3876 let mut ordered_moves = legal_moves.clone();
3879
3880 ordered_moves.sort_by(|a, b| {
3882 let a_is_capture = board.piece_on(a.get_dest()).is_some();
3883 let b_is_capture = board.piece_on(b.get_dest()).is_some();
3884
3885 match (a_is_capture, b_is_capture) {
3886 (true, false) => std::cmp::Ordering::Less,
3887 (false, true) => std::cmp::Ordering::Greater,
3888 _ => {
3889 let a_centrality = move_centrality(a);
3890 let b_centrality = move_centrality(b);
3891 b_centrality
3892 .partial_cmp(&a_centrality)
3893 .unwrap_or(std::cmp::Ordering::Equal)
3894 }
3895 }
3896 });
3897
3898 for chess_move in ordered_moves.into_iter().take(num_recommendations) {
3899 let mut basic_eval = 0.0;
3901
3902 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
3904 basic_eval += match captured_piece {
3905 chess::Piece::Pawn => 1.0,
3906 chess::Piece::Knight | chess::Piece::Bishop => 3.0,
3907 chess::Piece::Rook => 5.0,
3908 chess::Piece::Queen => 9.0,
3909 chess::Piece::King => 100.0, };
3911 }
3912
3913 move_data.insert(chess_move, vec![(0.3, basic_eval)]); }
3915 }
3916 }
3917
3918 let mut recommendations = Vec::new();
3920
3921 for (chess_move, outcomes) in move_data {
3922 if outcomes.is_empty() {
3923 continue;
3924 }
3925
3926 let mut weighted_sum = 0.0;
3928 let mut weight_sum = 0.0;
3929
3930 for &(similarity, outcome) in &outcomes {
3931 weighted_sum += similarity * outcome;
3932 weight_sum += similarity;
3933 }
3934
3935 let average_outcome = if weight_sum > 0.0 {
3936 weighted_sum / weight_sum
3937 } else {
3938 0.0
3939 };
3940
3941 let avg_similarity =
3943 outcomes.iter().map(|(s, _)| s).sum::<f32>() / outcomes.len() as f32;
3944 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 {
3948 chess_move,
3949 confidence: confidence.min(1.0), from_similar_position_count: outcomes.len(),
3951 average_outcome,
3952 });
3953 }
3954
3955 recommendations.sort_by(|a, b| {
3958 match board.side_to_move() {
3959 chess::Color::White => {
3960 b.average_outcome
3962 .partial_cmp(&a.average_outcome)
3963 .unwrap_or(std::cmp::Ordering::Equal)
3964 }
3965 chess::Color::Black => {
3966 a.average_outcome
3968 .partial_cmp(&b.average_outcome)
3969 .unwrap_or(std::cmp::Ordering::Equal)
3970 }
3971 }
3972 });
3973
3974 recommendations = self.apply_hanging_piece_safety_checks(board, recommendations);
3976
3977 recommendations.truncate(num_recommendations);
3979 recommendations
3980 }
3981
3982 fn apply_hanging_piece_safety_checks(
3985 &mut self,
3986 board: &Board,
3987 mut recommendations: Vec<MoveRecommendation>,
3988 ) -> Vec<MoveRecommendation> {
3989 use chess::{MoveGen, Piece};
3990
3991 for recommendation in &mut recommendations {
3992 let mut safety_penalty = 0.0;
3993
3994 let mut temp_board = *board;
3996 temp_board = temp_board.make_move_new(recommendation.chess_move);
3997
3998 let our_color = board.side_to_move();
4000 let opponent_color = !our_color;
4001
4002 let opponent_moves: Vec<chess::ChessMove> = MoveGen::new_legal(&temp_board).collect();
4004
4005 for square in chess::ALL_SQUARES {
4007 if let Some(piece) = temp_board.piece_on(square) {
4008 if temp_board.color_on(square) == Some(our_color) {
4009 let piece_value = match piece {
4011 Piece::Pawn => 1.0,
4012 Piece::Knight | Piece::Bishop => 3.0,
4013 Piece::Rook => 5.0,
4014 Piece::Queen => 9.0,
4015 Piece::King => 0.0, };
4017
4018 let can_be_captured =
4020 opponent_moves.iter().any(|&mv| mv.get_dest() == square);
4021
4022 if can_be_captured {
4023 let is_defended =
4025 self.is_piece_defended(&temp_board, square, our_color);
4026
4027 if !is_defended {
4028 safety_penalty += piece_value * 2.0; } else {
4031 safety_penalty += piece_value * 0.1; }
4034 }
4035 }
4036 }
4037 }
4038
4039 let original_threats = self.find_immediate_threats(board, opponent_color);
4041 let resolved_threats =
4042 self.count_resolved_threats(board, &temp_board, &original_threats);
4043
4044 if !original_threats.is_empty() && resolved_threats == 0 {
4046 safety_penalty += 2.0; }
4048
4049 let penalty_factor = 1.0 - (safety_penalty * 0.2_f32).min(0.8);
4051 recommendation.confidence *= penalty_factor;
4052 recommendation.confidence = recommendation.confidence.max(0.1);
4053
4054 recommendation.average_outcome -= safety_penalty;
4056 }
4057
4058 recommendations.sort_by(|a, b| {
4060 let confidence_cmp = b
4062 .confidence
4063 .partial_cmp(&a.confidence)
4064 .unwrap_or(std::cmp::Ordering::Equal);
4065 if confidence_cmp != std::cmp::Ordering::Equal {
4066 return confidence_cmp;
4067 }
4068
4069 match board.side_to_move() {
4071 chess::Color::White => b
4072 .average_outcome
4073 .partial_cmp(&a.average_outcome)
4074 .unwrap_or(std::cmp::Ordering::Equal),
4075 chess::Color::Black => a
4076 .average_outcome
4077 .partial_cmp(&b.average_outcome)
4078 .unwrap_or(std::cmp::Ordering::Equal),
4079 }
4080 });
4081
4082 recommendations
4083 }
4084
4085 fn is_piece_defended(
4087 &self,
4088 board: &Board,
4089 square: chess::Square,
4090 our_color: chess::Color,
4091 ) -> bool {
4092 use chess::ALL_SQUARES;
4093
4094 for source_square in ALL_SQUARES {
4096 if let Some(piece) = board.piece_on(source_square) {
4097 if board.color_on(source_square) == Some(our_color) {
4098 if self.can_piece_attack(board, piece, source_square, square) {
4100 return true;
4101 }
4102 }
4103 }
4104 }
4105
4106 false
4107 }
4108
4109 fn can_piece_attack(
4111 &self,
4112 board: &Board,
4113 piece: chess::Piece,
4114 from: chess::Square,
4115 to: chess::Square,
4116 ) -> bool {
4117 use chess::Piece;
4118
4119 match piece {
4122 Piece::Pawn => {
4123 let from_file = from.get_file().to_index();
4125 let from_rank = from.get_rank().to_index();
4126 let to_file = to.get_file().to_index();
4127 let to_rank = to.get_rank().to_index();
4128
4129 let file_diff = (to_file as i32 - from_file as i32).abs();
4130 let rank_diff = to_rank as i32 - from_rank as i32;
4131
4132 file_diff == 1 && {
4134 match board.color_on(from).unwrap() {
4135 chess::Color::White => rank_diff == 1,
4136 chess::Color::Black => rank_diff == -1,
4137 }
4138 }
4139 }
4140 Piece::Knight => {
4141 let from_file = from.get_file().to_index() as i32;
4143 let from_rank = from.get_rank().to_index() as i32;
4144 let to_file = to.get_file().to_index() as i32;
4145 let to_rank = to.get_rank().to_index() as i32;
4146
4147 let file_diff = (to_file - from_file).abs();
4148 let rank_diff = (to_rank - from_rank).abs();
4149
4150 (file_diff == 2 && rank_diff == 1) || (file_diff == 1 && rank_diff == 2)
4151 }
4152 Piece::Bishop => {
4153 self.is_diagonal_clear(board, from, to)
4155 }
4156 Piece::Rook => {
4157 self.is_straight_clear(board, from, to)
4159 }
4160 Piece::Queen => {
4161 self.is_diagonal_clear(board, from, to) || self.is_straight_clear(board, from, to)
4163 }
4164 Piece::King => {
4165 let from_file = from.get_file().to_index() as i32;
4167 let from_rank = from.get_rank().to_index() as i32;
4168 let to_file = to.get_file().to_index() as i32;
4169 let to_rank = to.get_rank().to_index() as i32;
4170
4171 let file_diff = (to_file - from_file).abs();
4172 let rank_diff = (to_rank - from_rank).abs();
4173
4174 file_diff <= 1 && rank_diff <= 1 && (file_diff != 0 || rank_diff != 0)
4175 }
4176 }
4177 }
4178
4179 fn is_diagonal_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
4181 let from_file = from.get_file().to_index() as i32;
4182 let from_rank = from.get_rank().to_index() as i32;
4183 let to_file = to.get_file().to_index() as i32;
4184 let to_rank = to.get_rank().to_index() as i32;
4185
4186 let file_diff = to_file - from_file;
4187 let rank_diff = to_rank - from_rank;
4188
4189 if file_diff.abs() != rank_diff.abs() || file_diff == 0 {
4191 return false;
4192 }
4193
4194 let file_step = if file_diff > 0 { 1 } else { -1 };
4195 let rank_step = if rank_diff > 0 { 1 } else { -1 };
4196
4197 let steps = file_diff.abs();
4198
4199 for i in 1..steps {
4201 let check_file = from_file + i * file_step;
4202 let check_rank = from_rank + i * rank_step;
4203
4204 let check_square = chess::Square::make_square(
4205 chess::Rank::from_index(check_rank as usize),
4206 chess::File::from_index(check_file as usize),
4207 );
4208 if board.piece_on(check_square).is_some() {
4209 return false; }
4211 }
4212
4213 true
4214 }
4215
4216 fn is_straight_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
4218 let from_file = from.get_file().to_index() as i32;
4219 let from_rank = from.get_rank().to_index() as i32;
4220 let to_file = to.get_file().to_index() as i32;
4221 let to_rank = to.get_rank().to_index() as i32;
4222
4223 if from_file != to_file && from_rank != to_rank {
4225 return false;
4226 }
4227
4228 if from_file == to_file {
4229 let start_rank = from_rank.min(to_rank);
4231 let end_rank = from_rank.max(to_rank);
4232
4233 for rank in (start_rank + 1)..end_rank {
4234 let check_square = chess::Square::make_square(
4235 chess::Rank::from_index(rank as usize),
4236 chess::File::from_index(from_file as usize),
4237 );
4238 if board.piece_on(check_square).is_some() {
4239 return false; }
4241 }
4242 } else {
4243 let start_file = from_file.min(to_file);
4245 let end_file = from_file.max(to_file);
4246
4247 for file in (start_file + 1)..end_file {
4248 let check_square = chess::Square::make_square(
4249 chess::Rank::from_index(from_rank as usize),
4250 chess::File::from_index(file as usize),
4251 );
4252 if board.piece_on(check_square).is_some() {
4253 return false; }
4255 }
4256 }
4257
4258 true
4259 }
4260
4261 fn find_immediate_threats(
4263 &self,
4264 board: &Board,
4265 opponent_color: chess::Color,
4266 ) -> Vec<(chess::Square, f32)> {
4267 use chess::MoveGen;
4268
4269 let mut threats = Vec::new();
4270
4271 let opponent_moves: Vec<chess::ChessMove> = MoveGen::new_legal(board).collect();
4273
4274 for mv in opponent_moves {
4275 let target_square = mv.get_dest();
4276 if let Some(piece) = board.piece_on(target_square) {
4277 if board.color_on(target_square) == Some(!opponent_color) {
4278 let piece_value = match piece {
4280 chess::Piece::Pawn => 1.0,
4281 chess::Piece::Knight | chess::Piece::Bishop => 3.0,
4282 chess::Piece::Rook => 5.0,
4283 chess::Piece::Queen => 9.0,
4284 chess::Piece::King => 100.0,
4285 };
4286 threats.push((target_square, piece_value));
4287 }
4288 }
4289 }
4290
4291 threats
4292 }
4293
4294 fn count_resolved_threats(
4296 &self,
4297 original_board: &Board,
4298 new_board: &Board,
4299 original_threats: &[(chess::Square, f32)],
4300 ) -> usize {
4301 let mut resolved = 0;
4302
4303 for &(threatened_square, _value) in original_threats {
4304 let piece_still_there =
4306 new_board.piece_on(threatened_square) == original_board.piece_on(threatened_square);
4307
4308 if !piece_still_there {
4309 resolved += 1;
4311 } else {
4312 let still_threatened = self
4314 .find_immediate_threats(new_board, new_board.side_to_move())
4315 .iter()
4316 .any(|&(square, _)| square == threatened_square);
4317
4318 if !still_threatened {
4319 resolved += 1;
4320 }
4321 }
4322 }
4323
4324 resolved
4325 }
4326
4327 pub fn recommend_legal_moves(
4329 &mut self,
4330 board: &Board,
4331 num_recommendations: usize,
4332 ) -> Vec<MoveRecommendation> {
4333 use chess::MoveGen;
4334
4335 let legal_moves: std::collections::HashSet<ChessMove> = MoveGen::new_legal(board).collect();
4337
4338 let all_recommendations = self.recommend_moves(board, num_recommendations * 2); all_recommendations
4342 .into_iter()
4343 .filter(|rec| legal_moves.contains(&rec.chess_move))
4344 .take(num_recommendations)
4345 .collect()
4346 }
4347
4348 pub fn enable_persistence<P: AsRef<Path>>(
4350 &mut self,
4351 db_path: P,
4352 ) -> Result<(), Box<dyn std::error::Error>> {
4353 let database = Database::new(db_path)?;
4354 self.database = Some(database);
4355 println!("Persistence enabled");
4356 Ok(())
4357 }
4358
4359 pub fn save_to_database(&self) -> Result<(), Box<dyn std::error::Error>> {
4361 let db = self
4362 .database
4363 .as_ref()
4364 .ok_or("Database not enabled. Call enable_persistence() first.")?;
4365
4366 println!("š¾ Saving engine state to database (batch mode)...");
4367
4368 let current_time = std::time::SystemTime::now()
4370 .duration_since(std::time::UNIX_EPOCH)?
4371 .as_secs() as i64;
4372
4373 let mut position_data_batch = Vec::with_capacity(self.position_boards.len());
4374
4375 for (i, board) in self.position_boards.iter().enumerate() {
4376 if i < self.position_vectors.len() && i < self.position_evaluations.len() {
4377 let vector = self.position_vectors[i].as_slice().unwrap();
4378 let position_data = PositionData {
4379 fen: board.to_string(),
4380 vector: vector.iter().map(|&x| x as f64).collect(),
4381 evaluation: Some(self.position_evaluations[i] as f64),
4382 compressed_vector: None,
4383 created_at: current_time,
4384 };
4385 position_data_batch.push(position_data);
4386 }
4387 }
4388
4389 if !position_data_batch.is_empty() {
4391 let saved_count = db.save_positions_batch(&position_data_batch)?;
4392 println!("š Batch saved {saved_count} positions");
4393 }
4394
4395 if let Some(ref lsh) = self.lsh_index {
4397 lsh.save_to_database(db)?;
4398 }
4399
4400
4401 println!("ā
Engine state saved successfully (batch optimized)");
4402 Ok(())
4403 }
4404
4405 pub fn load_from_database(&mut self) -> Result<(), Box<dyn std::error::Error>> {
4407 let db = self
4408 .database
4409 .as_ref()
4410 .ok_or("Database not enabled. Call enable_persistence() first.")?;
4411
4412 println!("Loading engine state from database...");
4413
4414 let positions = db.load_all_positions()?;
4416 for position_data in positions {
4417 if let Ok(board) = Board::from_str(&position_data.fen) {
4418 let vector: Vec<f32> = position_data.vector.iter().map(|&x| x as f32).collect();
4419 let vector_array = Array1::from(vector);
4420 let mut evaluation = position_data.evaluation.unwrap_or(0.0) as f32;
4421
4422 if evaluation.abs() > 15.0 {
4426 evaluation /= 100.0;
4427 }
4428
4429 self.similarity_search
4431 .add_position(vector_array.clone(), evaluation);
4432
4433 self.position_vectors.push(vector_array);
4435 self.position_boards.push(board);
4436 self.position_evaluations.push(evaluation);
4437 }
4438 }
4439
4440 if self.use_lsh {
4442 let positions_for_lsh: Vec<(Array1<f32>, f32)> = self
4443 .position_vectors
4444 .iter()
4445 .zip(self.position_evaluations.iter())
4446 .map(|(v, &e)| (v.clone(), e))
4447 .collect();
4448
4449 match LSH::load_from_database(db, &positions_for_lsh)? {
4450 Some(lsh) => {
4451 self.lsh_index = Some(lsh);
4452 println!("Loaded LSH configuration from database");
4453 }
4454 None => {
4455 println!("No LSH configuration found in database");
4456 }
4457 }
4458 }
4459
4460
4461 println!(
4462 "Engine state loaded successfully ({} positions)",
4463 self.knowledge_base_size()
4464 );
4465 Ok(())
4466 }
4467
4468 pub fn new_with_persistence<P: AsRef<Path>>(
4470 vector_size: usize,
4471 db_path: P,
4472 ) -> Result<Self, Box<dyn std::error::Error>> {
4473 let mut engine = Self::new(vector_size);
4474 engine.enable_persistence(db_path)?;
4475
4476 match engine.load_from_database() {
4478 Ok(_) => {
4479 println!("Loaded existing engine from database");
4480 }
4481 Err(e) => {
4482 println!("Starting fresh engine (load failed: {e})");
4483 }
4484 }
4485
4486 Ok(engine)
4487 }
4488
4489 pub fn auto_save(&self) -> Result<(), Box<dyn std::error::Error>> {
4491 if self.database.is_some() {
4492 self.save_to_database()?;
4493 }
4494 Ok(())
4495 }
4496
4497 pub fn is_persistence_enabled(&self) -> bool {
4499 self.database.is_some()
4500 }
4501
4502 pub fn database_position_count(&self) -> Result<i64, Box<dyn std::error::Error>> {
4504 let db = self.database.as_ref().ok_or("Database not enabled")?;
4505 Ok(db.get_position_count()?)
4506 }
4507
4508 pub fn enable_tactical_search(&mut self, config: TacticalConfig) {
4510 self.tactical_search = Some(TacticalSearch::new(config));
4511 }
4512
4513 pub fn enable_tactical_search_default(&mut self) {
4515 self.tactical_search = Some(TacticalSearch::new_default());
4516 }
4517
4518 pub fn configure_hybrid_evaluation(&mut self, config: HybridConfig) {
4520 self.hybrid_config = config;
4521 }
4522
4523 pub fn is_tactical_search_enabled(&self) -> bool {
4525 self.tactical_search.is_some()
4526 }
4527
4528 pub fn enable_parallel_search(&mut self, num_threads: usize) {
4530 if let Some(ref mut tactical_search) = self.tactical_search {
4531 tactical_search.config.enable_parallel_search = true;
4532 tactical_search.config.num_threads = num_threads;
4533 println!("š§µ Parallel tactical search enabled with {num_threads} threads");
4534 }
4535 }
4536
4537 pub fn is_parallel_search_enabled(&self) -> bool {
4539 self.tactical_search
4540 .as_ref()
4541 .map(|ts| ts.config.enable_parallel_search)
4542 .unwrap_or(false)
4543 }
4544
4545 pub fn enable_strategic_evaluation(&mut self, config: StrategicConfig) {
4552 self.strategic_evaluator = Some(StrategicEvaluator::new(config));
4553 println!("šÆ Strategic evaluation enabled - engine will play proactively");
4554 }
4555
4556 pub fn enable_strategic_evaluation_default(&mut self) {
4558 self.enable_strategic_evaluation(StrategicConfig::default());
4559 }
4560
4561 pub fn enable_strategic_evaluation_aggressive(&mut self) {
4563 self.enable_strategic_evaluation(StrategicConfig::aggressive());
4564 println!("āļø Aggressive strategic evaluation enabled - maximum initiative focus");
4565 }
4566
4567 pub fn enable_strategic_evaluation_positional(&mut self) {
4569 self.enable_strategic_evaluation(StrategicConfig::positional());
4570 println!("š Positional strategic evaluation enabled - long-term planning focus");
4571 }
4572
4573 pub fn is_strategic_evaluation_enabled(&self) -> bool {
4575 self.strategic_evaluator.is_some()
4576 }
4577
4578 pub fn get_strategic_evaluation(&self, board: &Board) -> Option<StrategicEvaluation> {
4580 self.strategic_evaluator
4581 .as_ref()
4582 .map(|evaluator| evaluator.evaluate_strategic(board))
4583 }
4584
4585 pub fn generate_proactive_moves(&self, board: &Board) -> Vec<(ChessMove, f32)> {
4588 if let Some(ref evaluator) = self.strategic_evaluator {
4589 evaluator.generate_proactive_moves(board)
4590 } else {
4591 Vec::new()
4593 }
4594 }
4595
4596 pub fn should_play_aggressively(&self, board: &Board) -> bool {
4598 if let Some(ref evaluator) = self.strategic_evaluator {
4599 evaluator.should_play_aggressively(board)
4600 } else {
4601 false }
4603 }
4604
4605 pub fn enable_nnue(&mut self) -> Result<(), Box<dyn std::error::Error>> {
4627 self.enable_nnue_with_auto_load(true)
4628 }
4629
4630 pub fn enable_nnue_with_auto_load(
4632 &mut self,
4633 auto_load: bool,
4634 ) -> Result<(), Box<dyn std::error::Error>> {
4635 let config = NNUEConfig::default();
4636 let mut nnue = NNUE::new(config)?;
4637
4638 if auto_load {
4640 if let Err(e) = self.try_load_default_nnue_model(&mut nnue) {
4641 println!("š Default NNUE model not found, using fresh model: {}", e);
4642 println!(
4643 " š” Create one with: cargo run --bin train_nnue -- --output default_hybrid"
4644 );
4645 } else {
4646 println!("ā
Auto-loaded default NNUE model (default_hybrid.config)");
4647
4648 if !nnue.are_weights_loaded() {
4650 println!("ā ļø Weights not properly applied, will use quick training fallback");
4651 } else {
4652 println!("ā
Weights successfully applied to feature transformer");
4653 }
4654 }
4655 }
4656
4657 self.nnue = Some(nnue);
4658 Ok(())
4659 }
4660
4661 fn try_load_default_nnue_model(
4663 &self,
4664 nnue: &mut NNUE,
4665 ) -> Result<(), Box<dyn std::error::Error>> {
4666 let default_paths = [
4668 "default_hybrid", "production_hybrid", "hybrid_production_nnue", "chess_nnue_advanced", "trained_nnue_model", ];
4674
4675 for path in &default_paths {
4676 let config_path = format!("{}.config", path);
4677 if std::path::Path::new(&config_path).exists() {
4678 nnue.load_model(path)?;
4679 return Ok(());
4680 }
4681 }
4682
4683 Err("No default NNUE model found in standard locations".into())
4684 }
4685
4686 pub fn enable_nnue_with_config(
4688 &mut self,
4689 config: NNUEConfig,
4690 ) -> Result<(), Box<dyn std::error::Error>> {
4691 self.nnue = Some(NNUE::new(config)?);
4692 Ok(())
4693 }
4694
4695 pub fn enable_nnue_with_model(
4697 &mut self,
4698 model_path: &str,
4699 ) -> Result<(), Box<dyn std::error::Error>> {
4700 let config = NNUEConfig::default();
4701 let mut nnue = NNUE::new(config)?;
4702 nnue.load_model(model_path)?;
4703 self.nnue = Some(nnue);
4704 Ok(())
4705 }
4706
4707 pub fn quick_fix_nnue_if_needed(&mut self) -> Result<(), Box<dyn std::error::Error>> {
4709 if let Some(ref mut nnue) = self.nnue {
4710 if !nnue.are_weights_loaded() {
4711 let training_positions = vec![(chess::Board::default(), 0.0)];
4713
4714 let mut positions = training_positions;
4716 if let Ok(board) = chess::Board::from_str(
4717 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
4718 ) {
4719 positions.push((board, 0.25));
4720 }
4721 if let Ok(board) = chess::Board::from_str("8/8/8/8/8/8/1K6/k6Q w - - 0 1") {
4722 positions.push((board, 9.0));
4723 }
4724
4725 nnue.quick_fix_training(&positions)?;
4726 }
4727 }
4728 Ok(())
4729 }
4730
4731 pub fn configure_nnue(&mut self, config: NNUEConfig) -> Result<(), Box<dyn std::error::Error>> {
4733 if self.nnue.is_some() {
4734 self.nnue = Some(NNUE::new(config)?);
4735 Ok(())
4736 } else {
4737 Err("NNUE must be enabled first before configuring".into())
4738 }
4739 }
4740
4741 pub fn is_nnue_enabled(&self) -> bool {
4743 self.nnue.is_some()
4744 }
4745
4746 pub fn train_nnue(
4748 &mut self,
4749 positions: &[(Board, f32)],
4750 ) -> Result<f32, Box<dyn std::error::Error>> {
4751 if let Some(ref mut nnue) = self.nnue {
4752 let loss = nnue.train_batch(positions)?;
4753 Ok(loss)
4754 } else {
4755 Err("NNUE must be enabled before training".into())
4756 }
4757 }
4758
4759 pub fn hybrid_config(&self) -> &HybridConfig {
4761 &self.hybrid_config
4762 }
4763
4764 pub fn is_opening_book_enabled(&self) -> bool {
4766 self.opening_book.is_some()
4767 }
4768
4769 pub fn self_play_training(
4771 &mut self,
4772 config: training::SelfPlayConfig,
4773 ) -> Result<usize, Box<dyn std::error::Error>> {
4774 let mut trainer = training::SelfPlayTrainer::new(config);
4775 let new_data = trainer.generate_training_data(self);
4776
4777 let positions_added = new_data.data.len();
4778
4779 for data in &new_data.data {
4781 self.add_position(&data.board, data.evaluation);
4782 }
4783
4784 if self.database.is_some() {
4786 match self.save_to_database() {
4787 Ok(_) => println!("š¾ Saved {positions_added} positions to database"),
4788 Err(_e) => println!("Loading complete"),
4789 }
4790 }
4791
4792 println!("š§ Self-play training complete: {positions_added} new positions learned");
4793 Ok(positions_added)
4794 }
4795
4796 pub fn continuous_self_play(
4798 &mut self,
4799 config: training::SelfPlayConfig,
4800 iterations: usize,
4801 save_path: Option<&str>,
4802 ) -> Result<usize, Box<dyn std::error::Error>> {
4803 let mut total_positions = 0;
4804 let mut trainer = training::SelfPlayTrainer::new(config.clone());
4805
4806 println!("š Starting continuous self-play training for {iterations} iterations...");
4807
4808 for iteration in 1..=iterations {
4809 println!("\n--- Self-Play Iteration {iteration}/{iterations} ---");
4810
4811 let new_data = trainer.generate_training_data(self);
4813 let batch_size = new_data.data.len();
4814
4815 for data in &new_data.data {
4817 self.add_position(&data.board, data.evaluation);
4818 }
4819
4820 total_positions += batch_size;
4821
4822 println!(
4823 "ā
Iteration {}: Added {} positions (total: {})",
4824 iteration,
4825 batch_size,
4826 self.knowledge_base_size()
4827 );
4828
4829 if iteration % 5 == 0 || iteration == iterations {
4831 if let Some(path) = save_path {
4833 match self.save_training_data_binary(path) {
4834 Ok(_) => println!("š¾ Progress saved to {path} (binary format)"),
4835 Err(_e) => println!("Loading complete"),
4836 }
4837 }
4838
4839 if self.database.is_some() {
4841 match self.save_to_database() {
4842 Ok(_) => println!(
4843 "š¾ Database synchronized ({} total positions)",
4844 self.knowledge_base_size()
4845 ),
4846 Err(_e) => println!("Loading complete"),
4847 }
4848 }
4849 }
4850
4851 }
4852
4853 println!("\nš Continuous self-play complete: {total_positions} total new positions");
4854 Ok(total_positions)
4855 }
4856
4857 pub fn adaptive_self_play(
4859 &mut self,
4860 base_config: training::SelfPlayConfig,
4861 target_strength: f32,
4862 ) -> Result<usize, Box<dyn std::error::Error>> {
4863 let mut current_config = base_config;
4864 let mut total_positions = 0;
4865 let mut iteration = 1;
4866
4867 println!(
4868 "šÆ Starting adaptive self-play training (target strength: {target_strength:.2})..."
4869 );
4870
4871 loop {
4872 println!("\n--- Adaptive Iteration {iteration} ---");
4873
4874 let positions_added = self.self_play_training(current_config.clone())?;
4876 total_positions += positions_added;
4877
4878 if self.database.is_some() {
4880 match self.save_to_database() {
4881 Ok(_) => println!("š¾ Adaptive training progress saved to database"),
4882 Err(_e) => println!("Loading complete"),
4883 }
4884 }
4885
4886 let current_strength = self.knowledge_base_size() as f32 / 10000.0; println!(
4890 "š Current strength estimate: {current_strength:.2} (target: {target_strength:.2})"
4891 );
4892
4893 if current_strength >= target_strength {
4894 println!("š Target strength reached!");
4895 break;
4896 }
4897
4898 current_config.exploration_factor *= 0.95; current_config.temperature *= 0.98; current_config.games_per_iteration =
4902 (current_config.games_per_iteration as f32 * 1.1) as usize; iteration += 1;
4905
4906 if iteration > 50 {
4907 println!("ā ļø Maximum iterations reached");
4908 break;
4909 }
4910 }
4911
4912 Ok(total_positions)
4913 }
4914}
4915
4916#[cfg(test)]
4917mod tests {
4918 use super::*;
4919 use chess::Board;
4920
4921 #[test]
4922 fn test_engine_creation() {
4923 let engine = ChessVectorEngine::new(1024);
4924 assert_eq!(engine.knowledge_base_size(), 0);
4925 }
4926
4927 #[test]
4928 fn test_add_and_search() {
4929 let mut engine = ChessVectorEngine::new(1024);
4930 let board = Board::default();
4931
4932 engine.add_position(&board, 0.0);
4933 assert_eq!(engine.knowledge_base_size(), 1);
4934
4935 let similar = engine.find_similar_positions(&board, 1);
4936 assert_eq!(similar.len(), 1);
4937 }
4938
4939 #[test]
4940 fn test_evaluation() {
4941 let mut engine = ChessVectorEngine::new(1024);
4942 let board = Board::default();
4943
4944 engine.add_position(&board, 0.5);
4946
4947 let evaluation = engine.evaluate_position(&board);
4948 assert!(evaluation.is_some());
4949 let eval_value = evaluation.unwrap();
4952 assert!(
4953 eval_value > -1000.0 && eval_value < 1000.0,
4954 "Evaluation should be reasonable: {}",
4955 eval_value
4956 );
4957 }
4958
4959 #[test]
4960 fn test_move_recommendations() {
4961 let mut engine = ChessVectorEngine::new(1024);
4962 let board = Board::default();
4963
4964 use chess::ChessMove;
4966 use std::str::FromStr;
4967 let mov = ChessMove::from_str("e2e4").unwrap();
4968 engine.add_position_with_move(&board, 0.0, Some(mov), Some(0.8));
4969
4970 let recommendations = engine.recommend_moves(&board, 3);
4971 assert!(!recommendations.is_empty());
4972
4973 let legal_recommendations = engine.recommend_legal_moves(&board, 3);
4975 assert!(!legal_recommendations.is_empty());
4976 }
4977
4978 #[test]
4979 fn test_empty_knowledge_base_fallback() {
4980 let mut engine = ChessVectorEngine::new(1024);
4982
4983 use std::str::FromStr;
4985 let board =
4986 Board::from_str("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 0 1")
4987 .unwrap();
4988
4989 let recommendations = engine.recommend_moves(&board, 5);
4991 assert!(
4992 !recommendations.is_empty(),
4993 "recommend_moves should not return empty even with no training data"
4994 );
4995 assert_eq!(
4996 recommendations.len(),
4997 5,
4998 "Should return exactly 5 recommendations"
4999 );
5000
5001 for rec in &recommendations {
5003 assert!(rec.confidence > 0.0, "Confidence should be greater than 0");
5004 assert_eq!(
5005 rec.from_similar_position_count, 1,
5006 "Should have count of 1 for fallback"
5007 );
5008 assert!(
5011 rec.average_outcome.abs() < 1000.0,
5012 "Average outcome should be reasonable: {}",
5013 rec.average_outcome
5014 );
5015 }
5016
5017 let starting_board = Board::default();
5019 let starting_recommendations = engine.recommend_moves(&starting_board, 3);
5020 assert!(
5021 !starting_recommendations.is_empty(),
5022 "Should work for starting position too"
5023 );
5024
5025 use chess::MoveGen;
5027 let legal_moves: std::collections::HashSet<_> = MoveGen::new_legal(&board).collect();
5028 for rec in &recommendations {
5029 assert!(
5030 legal_moves.contains(&rec.chess_move),
5031 "All recommended moves should be legal"
5032 );
5033 }
5034 }
5035
5036 #[test]
5037 fn test_opening_book_integration() {
5038 let mut engine = ChessVectorEngine::new(1024);
5039
5040 engine.enable_opening_book();
5042 assert!(engine.opening_book.is_some());
5043
5044 let board = Board::default();
5046 assert!(engine.is_opening_position(&board));
5047
5048 let entry = engine.get_opening_entry(&board);
5049 assert!(entry.is_some());
5050
5051 let stats = engine.opening_book_stats();
5052 assert!(stats.is_some());
5053 assert!(stats.unwrap().total_openings > 0);
5054
5055 let recommendations = engine.recommend_moves(&board, 3);
5057 assert!(!recommendations.is_empty());
5058 assert!(recommendations[0].confidence > 0.7); }
5060
5061
5062 #[test]
5063 fn test_lsh_integration() {
5064 let mut engine = ChessVectorEngine::new(1024);
5065
5066 let board = Board::default();
5068 for i in 0..50 {
5069 engine.add_position(&board, i as f32 * 0.02);
5070 }
5071
5072 engine.enable_lsh(4, 8);
5074
5075 let similar = engine.find_similar_positions(&board, 5);
5077 assert!(!similar.is_empty());
5078 assert!(similar.len() <= 5);
5079
5080 let eval = engine.evaluate_position(&board);
5082 assert!(eval.is_some());
5083 }
5084
5085
5086 #[test]
5111 fn test_position_with_move_storage() {
5112 let mut engine = ChessVectorEngine::new(1024);
5113 let board = Board::default();
5114
5115 use chess::ChessMove;
5116 use std::str::FromStr;
5117 let move1 = ChessMove::from_str("e2e4").unwrap();
5118 let move2 = ChessMove::from_str("d2d4").unwrap();
5119
5120 engine.add_position_with_move(&board, 0.0, Some(move1), Some(0.7));
5122 engine.add_position_with_move(&board, 0.1, Some(move2), Some(0.6));
5123
5124 assert_eq!(engine.position_moves.len(), 2);
5126
5127 let recommendations = engine.recommend_moves(&board, 5);
5129 let _move_strings: Vec<String> = recommendations
5130 .iter()
5131 .map(|r| r.chess_move.to_string())
5132 .collect();
5133
5134 assert!(!recommendations.is_empty());
5136 }
5137
5138 #[test]
5139 fn test_performance_regression_basic() {
5140 use std::time::Instant;
5141
5142 let mut engine = ChessVectorEngine::new(1024);
5143 let board = Board::default();
5144
5145 for i in 0..100 {
5147 engine.add_position(&board, i as f32 * 0.01);
5148 }
5149
5150 let start = Instant::now();
5152
5153 for _ in 0..100 {
5155 engine.add_position(&board, 0.0);
5156 }
5157
5158 let encoding_time = start.elapsed();
5159
5160 let start = Instant::now();
5162 for _ in 0..10 {
5163 engine.find_similar_positions(&board, 5);
5164 }
5165 let search_time = start.elapsed();
5166
5167 assert!(
5169 encoding_time.as_millis() < 10000,
5170 "Position encoding too slow: {}ms",
5171 encoding_time.as_millis()
5172 );
5173 assert!(
5174 search_time.as_millis() < 5000,
5175 "Search too slow: {}ms",
5176 search_time.as_millis()
5177 );
5178 }
5179
5180 #[test]
5181 fn test_memory_usage_reasonable() {
5182 let mut engine = ChessVectorEngine::new(1024);
5183 let board = Board::default();
5184
5185 let initial_size = engine.knowledge_base_size();
5187
5188 for i in 0..1000 {
5189 engine.add_position(&board, i as f32 * 0.001);
5190 }
5191
5192 let final_size = engine.knowledge_base_size();
5193 assert_eq!(final_size, initial_size + 1000);
5194
5195 assert!(final_size > initial_size);
5197 }
5198
5199 #[test]
5200 fn test_incremental_training() {
5201 use std::str::FromStr;
5202
5203 let mut engine = ChessVectorEngine::new(1024);
5204 let board1 = Board::default();
5205 let board2 =
5206 Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap();
5207
5208 engine.add_position(&board1, 0.0);
5210 engine.add_position(&board2, 0.2);
5211 assert_eq!(engine.knowledge_base_size(), 2);
5212
5213 let mut dataset = crate::training::TrainingDataset::new();
5215 dataset.add_position(board1, 0.1, 15, 1); dataset.add_position(
5217 Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
5218 .unwrap(),
5219 0.3,
5220 15,
5221 2,
5222 ); engine.train_from_dataset_incremental(&dataset);
5226
5227 assert_eq!(engine.knowledge_base_size(), 3);
5229
5230 let stats = engine.training_stats();
5232 assert_eq!(stats.total_positions, 3);
5233 assert_eq!(stats.unique_positions, 3);
5234 assert!(!stats.has_move_data); }
5236
5237 #[test]
5238 fn test_save_load_incremental() {
5239 use std::str::FromStr;
5240 use tempfile::tempdir;
5241
5242 let temp_dir = tempdir().unwrap();
5243 let file_path = temp_dir.path().join("test_training.json");
5244
5245 let mut engine1 = ChessVectorEngine::new(1024);
5247 engine1.add_position(&Board::default(), 0.0);
5248 engine1.add_position(
5249 &Board::from_str("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1").unwrap(),
5250 0.2,
5251 );
5252
5253 engine1.save_training_data(&file_path).unwrap();
5255
5256 let mut engine2 = ChessVectorEngine::new(1024);
5258 engine2.add_position(
5259 &Board::from_str("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
5260 .unwrap(),
5261 0.3,
5262 );
5263 assert_eq!(engine2.knowledge_base_size(), 1);
5264
5265 engine2.load_training_data_incremental(&file_path).unwrap();
5267
5268 assert_eq!(engine2.knowledge_base_size(), 3);
5270 }
5271
5272 #[test]
5273 fn test_training_stats() {
5274 use std::str::FromStr;
5275
5276 let mut engine = ChessVectorEngine::new(1024);
5277
5278 let stats = engine.training_stats();
5280 assert_eq!(stats.total_positions, 0);
5281 assert_eq!(stats.unique_positions, 0);
5282 assert!(!stats.has_move_data);
5283 assert!(!stats.lsh_enabled);
5284 assert!(stats.opening_book_enabled); engine.add_position(&Board::default(), 0.0);
5288 engine.add_position_with_move(
5289 &Board::default(),
5290 0.1,
5291 Some(ChessMove::from_str("e2e4").unwrap()),
5292 Some(0.8),
5293 );
5294
5295 engine.enable_opening_book();
5297 engine.enable_lsh(4, 8);
5298
5299 let stats = engine.training_stats();
5300 assert_eq!(stats.total_positions, 2);
5301 assert!(stats.has_move_data);
5302 assert!(stats.move_data_entries > 0);
5303 assert!(stats.lsh_enabled);
5304 assert!(stats.opening_book_enabled);
5305 }
5306
5307 #[test]
5308 fn test_tactical_search_integration() {
5309 let mut engine = ChessVectorEngine::new(1024);
5310 let board = Board::default();
5311
5312 assert!(engine.is_tactical_search_enabled());
5314
5315 engine.enable_tactical_search_default();
5317 assert!(engine.is_tactical_search_enabled());
5318
5319 let evaluation = engine.evaluate_position(&board);
5321 assert!(evaluation.is_some());
5322
5323 engine.add_position(&board, 0.5);
5325 let hybrid_evaluation = engine.evaluate_position(&board);
5326 assert!(hybrid_evaluation.is_some());
5327 }
5328
5329 #[test]
5330 fn test_hybrid_evaluation_configuration() {
5331 let mut engine = ChessVectorEngine::new(1024);
5332 let board = Board::default();
5333
5334 engine.enable_tactical_search_default();
5336
5337 let custom_config = HybridConfig {
5339 pattern_confidence_threshold: 0.9, enable_tactical_refinement: true,
5341 tactical_config: TacticalConfig::default(),
5342 pattern_weight: 0.8,
5343 min_similar_positions: 5,
5344 };
5345
5346 engine.configure_hybrid_evaluation(custom_config);
5347
5348 engine.add_position(&board, 0.3);
5350
5351 let evaluation = engine.evaluate_position(&board);
5352 assert!(evaluation.is_some());
5353
5354 let no_tactical_config = HybridConfig {
5356 enable_tactical_refinement: false,
5357 ..HybridConfig::default()
5358 };
5359
5360 engine.configure_hybrid_evaluation(no_tactical_config);
5361
5362 let pattern_only_evaluation = engine.evaluate_position(&board);
5363 assert!(pattern_only_evaluation.is_some());
5364 }
5365}