1use crate::strategic_evaluator::{StrategicConfig, StrategicEvaluator};
2use chess::{Board, ChessMove, Color, MoveGen, Square};
3use rayon::prelude::*;
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10enum GamePhase {
11 Opening,
12 Middlegame,
13 Endgame,
14}
15
16#[derive(Debug, Clone, Copy)]
18enum FileType {
19 Open,
20 SemiOpen,
21 Closed,
22}
23
24#[derive(Clone)]
26struct FixedTranspositionTable {
27 entries: Vec<Option<TranspositionEntry>>,
28 size: usize,
29 age: u8,
30}
31
32impl FixedTranspositionTable {
33 fn new(size_mb: usize) -> Self {
34 let entry_size = std::mem::size_of::<TranspositionEntry>();
35 let size = (size_mb * 1024 * 1024) / entry_size;
36
37 Self {
38 entries: vec![None; size],
39 size,
40 age: 0,
41 }
42 }
43
44 fn get(&self, hash: u64) -> Option<&TranspositionEntry> {
45 let index = (hash as usize) % self.size;
46 self.entries[index].as_ref()
47 }
48
49 fn insert(&mut self, hash: u64, entry: TranspositionEntry) {
50 let index = (hash as usize) % self.size;
51
52 let should_replace = match &self.entries[index] {
54 None => true,
55 Some(existing) => {
56 entry.depth >= existing.depth || (self.age.wrapping_sub(existing.age) > 4)
58 }
59 };
60
61 if should_replace {
62 self.entries[index] = Some(TranspositionEntry {
63 age: self.age,
64 ..entry
65 });
66 }
67 }
68
69 fn clear(&mut self) {
70 self.entries.fill(None);
71 self.age = self.age.wrapping_add(1);
72 }
73
74 fn len(&self) -> usize {
75 self.entries.iter().filter(|e| e.is_some()).count()
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct TacticalResult {
82 pub evaluation: f32,
83 pub best_move: Option<ChessMove>,
84 pub depth_reached: u32,
85 pub nodes_searched: u64,
86 pub time_elapsed: Duration,
87 pub is_tactical: bool,
88}
89
90#[derive(Debug, Clone)]
92pub struct TacticalConfig {
93 pub max_depth: u32,
95 pub max_time_ms: u64,
96 pub max_nodes: u64,
97 pub quiescence_depth: u32,
98
99 pub enable_transposition_table: bool,
101 pub enable_iterative_deepening: bool,
102 pub enable_aspiration_windows: bool,
103 pub enable_null_move_pruning: bool,
104 pub enable_late_move_reductions: bool,
105 pub enable_principal_variation_search: bool,
106 pub enable_parallel_search: bool,
107 pub enable_quiescence: bool,
108 pub num_threads: usize,
109
110 pub enable_hybrid_evaluation: bool,
112 pub pattern_confidence_threshold: f32,
113 pub pattern_weight: f32,
114
115 pub enable_futility_pruning: bool,
117 pub enable_razoring: bool,
118 pub enable_extended_futility_pruning: bool,
119 pub futility_margin_base: f32,
120 pub razor_margin: f32,
121 pub extended_futility_margin: f32,
122
123 pub enable_reverse_futility_pruning: bool,
125 pub enable_static_null_move_pruning: bool,
126 pub enable_move_count_pruning: bool,
127 pub enable_history_pruning: bool,
128 pub enable_see_pruning: bool,
129 pub reverse_futility_margin: f32,
130 pub move_count_base: u32,
131 pub move_count_depth_factor: f32,
132 pub history_pruning_threshold: i32,
133 pub see_pruning_threshold: i32,
134
135 pub null_move_reduction_depth: u32,
137 pub lmr_min_depth: u32,
138 pub lmr_min_moves: usize,
139 pub aspiration_window_size: f32,
140 pub aspiration_max_iterations: u32,
141 pub transposition_table_size_mb: usize,
142 pub killer_move_slots: usize,
143 pub history_max_depth: u32,
144
145 pub time_allocation_factor: f32,
147 pub time_extension_threshold: f32,
148 pub panic_time_factor: f32,
149
150 pub endgame_evaluation_weight: f32,
152 pub mobility_weight: f32,
153 pub king_safety_weight: f32,
154 pub pawn_structure_weight: f32,
155
156 pub enable_check_extensions: bool,
158 pub check_extension_depth: u32,
159 pub max_extensions_per_line: u32,
160
161 pub hybrid_evaluation_weight: f32, pub hybrid_move_ordering: bool, pub hybrid_pruning_threshold: f32, }
166
167impl Default for TacticalConfig {
168 fn default() -> Self {
169 Self {
171 max_depth: 10, max_time_ms: 1500, max_nodes: 1_000_000, quiescence_depth: 16, enable_transposition_table: true,
179 enable_iterative_deepening: true,
180 enable_aspiration_windows: true,
181 enable_null_move_pruning: true,
182 enable_late_move_reductions: true,
183 enable_principal_variation_search: true,
184 enable_parallel_search: true,
185 enable_quiescence: true,
186 num_threads: 4,
187
188 enable_futility_pruning: true,
190 enable_razoring: true,
191 enable_extended_futility_pruning: true,
192 futility_margin_base: 150.0, razor_margin: 300.0, extended_futility_margin: 50.0, enable_reverse_futility_pruning: true,
198 enable_static_null_move_pruning: true,
199 enable_move_count_pruning: true,
200 enable_history_pruning: true,
201 enable_see_pruning: true,
202 reverse_futility_margin: 120.0,
203 move_count_base: 3,
204 move_count_depth_factor: 2.0,
205 history_pruning_threshold: -1000,
206 see_pruning_threshold: -100,
207
208 null_move_reduction_depth: 3,
210 lmr_min_depth: 2,
211 lmr_min_moves: 3,
212 aspiration_window_size: 40.0, aspiration_max_iterations: 3, transposition_table_size_mb: 64,
215 killer_move_slots: 2,
216 history_max_depth: 20,
217
218 time_allocation_factor: 0.3, time_extension_threshold: 1.0, panic_time_factor: 1.5, endgame_evaluation_weight: 1.2,
225 mobility_weight: 1.0,
226 king_safety_weight: 1.3,
227 pawn_structure_weight: 0.9,
228
229 enable_check_extensions: true,
231 check_extension_depth: 3,
232 max_extensions_per_line: 10,
233
234 enable_hybrid_evaluation: true,
236 hybrid_evaluation_weight: 0.8, hybrid_move_ordering: true, hybrid_pruning_threshold: 0.6, pattern_confidence_threshold: 0.65, pattern_weight: 0.4, }
242 }
243}
244
245impl TacticalConfig {
246 pub fn hybrid_optimized() -> Self {
248 Self {
249 max_depth: 12, max_time_ms: 2000, max_nodes: 2_000_000, quiescence_depth: 20, aspiration_window_size: 40.0, aspiration_max_iterations: 3, futility_margin_base: 150.0, razor_margin: 300.0, extended_futility_margin: 50.0, time_allocation_factor: 0.3, time_extension_threshold: 1.0, panic_time_factor: 1.5, enable_hybrid_evaluation: true,
271 hybrid_evaluation_weight: 0.8, hybrid_move_ordering: true, hybrid_pruning_threshold: 0.6, ..Default::default()
276 }
277 }
278
279 pub fn nnue_assisted_fast() -> Self {
281 Self {
282 max_depth: 6, max_time_ms: 500, max_nodes: 100_000, quiescence_depth: 4, futility_margin_base: 100.0,
289 razor_margin: 200.0,
290 extended_futility_margin: 30.0,
291
292 aspiration_window_size: 60.0,
294 aspiration_max_iterations: 2,
295
296 time_allocation_factor: 0.2,
298 time_extension_threshold: 1.5,
299 panic_time_factor: 1.2,
300
301 enable_hybrid_evaluation: true,
303 hybrid_evaluation_weight: 0.9, hybrid_move_ordering: true, hybrid_pruning_threshold: 0.8, ..Default::default()
308 }
309 }
310
311 pub fn fast() -> Self {
313 Self {
314 max_depth: 10, max_time_ms: 1000, max_nodes: 500_000, quiescence_depth: 6, aspiration_window_size: 50.0,
319 transposition_table_size_mb: 64, num_threads: 1,
321 enable_hybrid_evaluation: true,
323 hybrid_evaluation_weight: 0.9,
324 hybrid_move_ordering: true,
325 hybrid_pruning_threshold: 0.75,
326 pattern_confidence_threshold: 0.6,
327 pattern_weight: 0.5,
328 enable_futility_pruning: true,
330 enable_razoring: true,
331 enable_extended_futility_pruning: true,
332 futility_margin_base: 80.0, razor_margin: 200.0, extended_futility_margin: 30.0, enable_null_move_pruning: true, enable_late_move_reductions: true, enable_principal_variation_search: true, enable_iterative_deepening: true,
340 enable_aspiration_windows: true,
341 enable_transposition_table: true,
342 enable_check_extensions: true,
344 check_extension_depth: 1,
345 max_extensions_per_line: 3,
346 ..Default::default()
347 }
348 }
349
350 pub fn competitive() -> Self {
352 Self {
353 max_depth: 15, max_time_ms: 1200, max_nodes: 2_000_000, quiescence_depth: 6, aspiration_window_size: 50.0, transposition_table_size_mb: 128, num_threads: 1,
360 enable_hybrid_evaluation: false, hybrid_evaluation_weight: 0.8,
363 hybrid_move_ordering: false,
364 hybrid_pruning_threshold: 0.7,
365 pattern_confidence_threshold: 0.6,
366 pattern_weight: 0.5,
367 enable_futility_pruning: true,
369 enable_razoring: true,
370 enable_extended_futility_pruning: true,
371 enable_null_move_pruning: true,
372 enable_late_move_reductions: true,
373 enable_principal_variation_search: true,
374 enable_iterative_deepening: true,
375 enable_aspiration_windows: true,
376 enable_transposition_table: true,
377 enable_quiescence: true,
378 enable_check_extensions: true,
379 futility_margin_base: 100.0,
381 razor_margin: 300.0,
382 extended_futility_margin: 50.0,
383 enable_reverse_futility_pruning: true,
385 enable_static_null_move_pruning: true,
386 enable_move_count_pruning: true,
387 enable_history_pruning: true,
388 enable_see_pruning: true,
389 reverse_futility_margin: 150.0,
390 move_count_base: 4,
391 move_count_depth_factor: 2.0,
392 history_pruning_threshold: -200,
393 see_pruning_threshold: -50,
394 check_extension_depth: 1,
396 max_extensions_per_line: 3,
397 ..Default::default()
398 }
399 }
400
401 pub fn ultra_fast() -> Self {
403 Self {
404 max_depth: 6, max_time_ms: 300, max_nodes: 100_000, quiescence_depth: 3, aspiration_window_size: 100.0,
409 transposition_table_size_mb: 32,
410 num_threads: 1,
411 enable_hybrid_evaluation: true,
413 hybrid_evaluation_weight: 0.95,
414 hybrid_move_ordering: true,
415 hybrid_pruning_threshold: 0.8,
416 pattern_confidence_threshold: 0.5,
417 pattern_weight: 0.7,
418 enable_futility_pruning: true,
420 enable_razoring: true,
421 futility_margin_base: 60.0,
422 razor_margin: 150.0,
423 enable_null_move_pruning: true,
425 enable_late_move_reductions: true,
426 enable_iterative_deepening: true,
427 enable_transposition_table: true,
428 enable_quiescence: true,
429 ..Default::default()
430 }
431 }
432
433 pub fn strong() -> Self {
435 Self {
436 max_depth: 18, max_time_ms: 30_000, max_nodes: 5_000_000, quiescence_depth: 12, aspiration_window_size: 25.0, transposition_table_size_mb: 256, num_threads: 8, ..Default::default()
444 }
445 }
446
447 pub fn traditional() -> Self {
449 Self {
450 max_depth: 14, max_time_ms: 5000, max_nodes: 2_000_000, quiescence_depth: 12, enable_transposition_table: true,
458 enable_iterative_deepening: true,
459 enable_aspiration_windows: true, enable_null_move_pruning: true,
461 enable_late_move_reductions: true,
462 enable_principal_variation_search: true,
463 enable_parallel_search: true,
464 enable_quiescence: true,
465 num_threads: 4,
466
467 enable_futility_pruning: true,
469 enable_razoring: true,
470 enable_extended_futility_pruning: true,
471 futility_margin_base: 200.0, razor_margin: 400.0, extended_futility_margin: 60.0, enable_reverse_futility_pruning: false,
477 enable_static_null_move_pruning: false,
478 enable_move_count_pruning: false,
479 enable_history_pruning: false,
480 enable_see_pruning: false,
481 reverse_futility_margin: 150.0,
482 move_count_base: 5,
483 move_count_depth_factor: 3.0,
484 history_pruning_threshold: -2000,
485 see_pruning_threshold: -200,
486
487 null_move_reduction_depth: 3, lmr_min_depth: 2, lmr_min_moves: 2, aspiration_window_size: 50.0, aspiration_max_iterations: 4, transposition_table_size_mb: 64, killer_move_slots: 2, history_max_depth: 20, time_allocation_factor: 0.4, time_extension_threshold: 0.8, panic_time_factor: 2.0, endgame_evaluation_weight: 1.2, mobility_weight: 1.0, king_safety_weight: 1.3, pawn_structure_weight: 0.9, enable_check_extensions: true, check_extension_depth: 3, max_extensions_per_line: 10, enable_hybrid_evaluation: false,
515 hybrid_evaluation_weight: 0.7, hybrid_move_ordering: false, hybrid_pruning_threshold: 0.5, pattern_confidence_threshold: 0.65, pattern_weight: 0.3, }
521 }
522
523 pub fn analysis() -> Self {
525 Self {
526 max_depth: 20,
527 max_time_ms: 60_000, max_nodes: 10_000_000, quiescence_depth: 10,
530 enable_aspiration_windows: false, transposition_table_size_mb: 512,
532 num_threads: std::thread::available_parallelism()
533 .map(|n| n.get())
534 .unwrap_or(4),
535 ..Default::default()
536 }
537 }
538
539 pub fn ultra_optimized() -> Self {
541 Self {
542 max_depth: 12, max_time_ms: 2000, max_nodes: 1_000_000, quiescence_depth: 8, enable_transposition_table: true,
550 enable_iterative_deepening: true,
551 enable_aspiration_windows: true,
552 enable_null_move_pruning: true,
553 enable_late_move_reductions: true,
554 enable_principal_variation_search: true,
555 enable_parallel_search: true,
556 enable_quiescence: true,
557 num_threads: 4, enable_futility_pruning: true,
561 enable_razoring: true,
562 enable_extended_futility_pruning: true,
563 futility_margin_base: 250.0, razor_margin: 500.0, extended_futility_margin: 80.0, enable_reverse_futility_pruning: true,
569 enable_static_null_move_pruning: true,
570 enable_move_count_pruning: true,
571 enable_history_pruning: true,
572 enable_see_pruning: true,
573 reverse_futility_margin: 100.0,
574 move_count_base: 2,
575 move_count_depth_factor: 1.0,
576 history_pruning_threshold: -300,
577 see_pruning_threshold: -30,
578
579 null_move_reduction_depth: 4, lmr_min_depth: 3, lmr_min_moves: 4, aspiration_window_size: 30.0, aspiration_max_iterations: 3, transposition_table_size_mb: 128, killer_move_slots: 2, history_max_depth: 16, time_allocation_factor: 0.3, time_extension_threshold: 1.0, panic_time_factor: 1.5, endgame_evaluation_weight: 1.0, mobility_weight: 0.8, king_safety_weight: 1.1, pawn_structure_weight: 0.7, enable_check_extensions: true,
602 check_extension_depth: 2, max_extensions_per_line: 6, enable_hybrid_evaluation: false, hybrid_evaluation_weight: 0.5, hybrid_move_ordering: false, hybrid_pruning_threshold: 0.4, pattern_confidence_threshold: 0.65, pattern_weight: 0.3, }
613 }
614}
615
616#[derive(Debug, Clone)]
618struct TranspositionEntry {
619 depth: u32,
620 evaluation: f32,
621 best_move: Option<ChessMove>,
622 node_type: NodeType,
623 age: u8, }
625
626#[derive(Debug, Clone, Copy)]
627enum NodeType {
628 Exact,
629 LowerBound,
630 UpperBound,
631}
632
633#[derive(Clone)]
635pub struct TacticalSearch {
636 pub config: TacticalConfig,
637 transposition_table: FixedTranspositionTable,
638 nodes_searched: u64,
639 start_time: Instant,
640 killer_moves: Vec<Vec<Option<ChessMove>>>, history_heuristic: HashMap<(Square, Square), u32>,
644 counter_moves: HashMap<(Square, Square), ChessMove>,
646 last_move: Option<ChessMove>,
648 strategic_evaluator: StrategicEvaluator,
650}
651
652impl TacticalSearch {
653 pub fn new(config: TacticalConfig) -> Self {
655 let max_depth = config.max_depth as usize + 1;
656 Self {
657 config,
658 transposition_table: FixedTranspositionTable::new(64), nodes_searched: 0,
660 start_time: Instant::now(),
661 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
663 counter_moves: HashMap::new(),
664 last_move: None,
665 strategic_evaluator: StrategicEvaluator::new(StrategicConfig::default()),
666 }
667 }
668
669 pub fn with_table_size(config: TacticalConfig, table_size_mb: usize) -> Self {
671 let max_depth = config.max_depth as usize + 1;
672 Self {
673 config,
674 transposition_table: FixedTranspositionTable::new(table_size_mb),
675 nodes_searched: 0,
676 start_time: Instant::now(),
677 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
679 counter_moves: HashMap::new(),
680 last_move: None,
681 strategic_evaluator: StrategicEvaluator::new(StrategicConfig::default()),
682 }
683 }
684
685 pub fn new_default() -> Self {
687 Self::new(TacticalConfig::default())
688 }
689
690 pub fn search_optimized(&mut self, board: &Board) -> TacticalResult {
692 self.nodes_searched = 0;
693 self.start_time = Instant::now();
694 self.transposition_table.clear();
695
696 let is_tactical = self.is_tactical_position(board);
698 let position_phase = self.detect_game_phase(board);
699
700 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
702 self.iterative_deepening_optimized(board, position_phase)
703 } else {
704 let (eval, mv) = self.minimax_optimized(
705 board,
706 self.config.max_depth,
707 f32::NEG_INFINITY,
708 f32::INFINITY,
709 board.side_to_move() == Color::White,
710 position_phase,
711 );
712 (eval, mv, self.config.max_depth)
713 };
714
715 TacticalResult {
716 evaluation,
717 best_move,
718 depth_reached,
719 nodes_searched: self.nodes_searched,
720 time_elapsed: self.start_time.elapsed(),
721 is_tactical,
722 }
723 }
724
725 fn get_move_order_score_optimized(&self, mv: ChessMove, board: &Board, depth: usize, game_phase: GamePhase) -> i32 {
727 let mut score = 0;
728
729 if depth < self.killer_moves.len() {
731 for killer in &self.killer_moves[depth] {
732 if let Some(killer_move) = killer {
733 if *killer_move == mv {
734 return 9000; }
736 }
737 }
738 }
739
740 let hash = board.get_hash();
742 if let Some(entry) = self.transposition_table.get(hash) {
743 if let Some(hash_move) = entry.best_move {
744 if hash_move == mv {
745 return 10000; }
747 }
748 }
749
750 let from_square = mv.get_source();
752 let to_square = mv.get_dest();
753
754 if let Some(captured_piece) = board.piece_on(to_square) {
755 let victim_value = match captured_piece {
756 chess::Piece::Queen => 900,
757 chess::Piece::Rook => 500,
758 chess::Piece::Bishop => 320,
759 chess::Piece::Knight => 300,
760 chess::Piece::Pawn => 100,
761 chess::Piece::King => 0, };
763
764 let attacker_value = if let Some(moving_piece) = board.piece_on(from_square) {
765 match moving_piece {
766 chess::Piece::Pawn => 1,
767 chess::Piece::Knight => 3,
768 chess::Piece::Bishop => 3,
769 chess::Piece::Rook => 5,
770 chess::Piece::Queen => 9,
771 chess::Piece::King => 10,
772 }
773 } else {
774 1
775 };
776
777 score += victim_value - attacker_value;
778 }
779
780 if let Some(promotion) = mv.get_promotion() {
782 score += match promotion {
783 chess::Piece::Queen => 800,
784 chess::Piece::Rook => 400,
785 chess::Piece::Bishop => 250,
786 chess::Piece::Knight => 250,
787 _ => 0,
788 };
789 }
790
791 let history_key = (from_square, to_square);
793 if let Some(&history_score) = self.history_heuristic.get(&history_key) {
794 let phase_multiplier = match game_phase {
795 GamePhase::Opening => 0.5, GamePhase::Middlegame => 1.0,
797 GamePhase::Endgame => 1.5, };
799 score += (history_score as f32 * phase_multiplier) as i32;
800 }
801
802 if let Some(last_move) = self.last_move {
804 let counter_key = (last_move.get_source(), last_move.get_dest());
805 if let Some(&counter_move) = self.counter_moves.get(&counter_key) {
806 if counter_move == mv {
807 score += 200;
808 }
809 }
810 }
811
812 let new_board = board.make_move_new(mv);
814 if new_board.checkers().popcnt() > 0 {
815 score += 100;
816 }
817
818 score
819 }
820
821 fn generate_ordered_moves_optimized(&mut self, board: &Board, depth: usize, game_phase: GamePhase) -> Vec<ChessMove> {
823 let mut moves: Vec<ChessMove> = MoveGen::new_legal(board).collect();
824
825 let mut move_scores: Vec<(ChessMove, i32)> = moves
827 .iter()
828 .map(|&mv| {
829 let score = self.get_move_order_score_optimized(mv, board, depth, game_phase);
830 (mv, score)
831 })
832 .collect();
833
834 move_scores.sort_unstable_by(|a, b| b.1.cmp(&a.1));
836
837 move_scores.into_iter().map(|(mv, _)| mv).collect()
839 }
840
841 fn iterative_deepening_optimized(&mut self, board: &Board, game_phase: GamePhase) -> (f32, Option<ChessMove>, u32) {
843 let mut best_move = None;
844 let mut best_evaluation = if board.side_to_move() == Color::White {
845 f32::NEG_INFINITY
846 } else {
847 f32::INFINITY
848 };
849 let mut depth_reached = 1;
850
851 let mut aspiration_window = self.config.aspiration_window_size;
853
854 for depth in 1..=self.config.max_depth {
855 if self.should_stop_search() {
856 break;
857 }
858
859 let (evaluation, mv) = if depth <= 3 || !self.config.enable_aspiration_windows {
860 self.minimax_optimized(
862 board,
863 depth,
864 f32::NEG_INFINITY,
865 f32::INFINITY,
866 board.side_to_move() == Color::White,
867 game_phase,
868 )
869 } else {
870 self.aspiration_search_optimized(board, depth, best_evaluation, aspiration_window, game_phase)
872 };
873
874 best_evaluation = evaluation;
875 if let Some(new_move) = mv {
876 best_move = Some(new_move);
877 }
878 depth_reached = depth;
879
880 if depth > 3 {
882 aspiration_window = self.config.aspiration_window_size;
883 }
884 }
885
886 (best_evaluation, best_move, depth_reached)
887 }
888
889 fn aspiration_search_optimized(
891 &mut self,
892 board: &Board,
893 depth: u32,
894 prev_eval: f32,
895 window_size: f32,
896 game_phase: GamePhase,
897 ) -> (f32, Option<ChessMove>) {
898 let mut alpha = prev_eval - window_size;
899 let mut beta = prev_eval + window_size;
900
901 for _ in 0..self.config.aspiration_max_iterations {
902 let (eval, mv) = self.minimax_optimized(board, depth, alpha, beta, board.side_to_move() == Color::White, game_phase);
903
904 if eval <= alpha {
905 alpha = f32::NEG_INFINITY;
907 } else if eval >= beta {
908 beta = f32::INFINITY;
910 } else {
911 return (eval, mv);
913 }
914 }
915
916 self.minimax_optimized(board, depth, f32::NEG_INFINITY, f32::INFINITY, board.side_to_move() == Color::White, game_phase)
918 }
919
920 fn minimax_optimized(
922 &mut self,
923 board: &Board,
924 depth: u32,
925 mut alpha: f32,
926 beta: f32,
927 maximizing: bool,
928 game_phase: GamePhase,
929 ) -> (f32, Option<ChessMove>) {
930 self.nodes_searched += 1;
931
932 if self.nodes_searched & 1023 == 0 && self.should_stop_search() {
934 return (0.0, None);
935 }
936
937 if depth == 0 {
939 if self.config.enable_quiescence {
940 return (self.quiescence_search_optimized(board, alpha, beta, maximizing, self.config.quiescence_depth), None);
941 } else {
942 return (self.evaluate_position_optimized(board, game_phase), None);
943 }
944 }
945
946 let hash = board.get_hash();
948 if let Some(entry) = self.transposition_table.get(hash) {
949 if entry.depth >= depth {
950 match entry.node_type {
951 NodeType::Exact => return (entry.evaluation, entry.best_move),
952 NodeType::LowerBound if entry.evaluation >= beta => return (entry.evaluation, entry.best_move),
953 NodeType::UpperBound if entry.evaluation <= alpha => return (entry.evaluation, entry.best_move),
954 _ => {}
955 }
956 }
957 }
958
959 if self.config.enable_null_move_pruning
961 && depth >= self.config.null_move_reduction_depth + 1
962 && board.checkers().popcnt() == 0 && self.has_non_pawn_material(board, board.side_to_move())
964 {
965 let null_board = board.null_move().unwrap_or(*board);
967 let (null_eval, _) = self.minimax_optimized(
968 &null_board,
969 depth - self.config.null_move_reduction_depth - 1,
970 -beta,
971 -alpha,
972 !maximizing,
973 game_phase,
974 );
975 let null_eval = -null_eval;
976
977 if null_eval >= beta {
978 return (beta, None); }
980 }
981
982 let moves = self.generate_ordered_moves_optimized(board, depth as usize, game_phase);
984
985 if moves.is_empty() {
986 return (self.evaluate_terminal_position(board), None);
987 }
988
989 let mut best_move = None;
990 let mut best_evaluation = if maximizing { f32::NEG_INFINITY } else { f32::INFINITY };
991 let mut alpha = alpha;
992 let original_alpha = alpha;
993 let mut moves_searched = 0;
994
995 for mv in moves {
996 let new_board = board.make_move_new(mv);
997 moves_searched += 1;
998
999 let evaluation = if moves_searched == 1 {
1000 let (eval, _) = self.minimax_optimized(&new_board, depth - 1, -beta, -alpha, !maximizing, game_phase);
1002 -eval
1003 } else {
1004 let should_reduce = self.config.enable_late_move_reductions
1006 && depth >= self.config.lmr_min_depth
1007 && moves_searched > self.config.lmr_min_moves
1008 && board.piece_on(mv.get_dest()).is_none() && mv.get_promotion().is_none() && new_board.checkers().popcnt() == 0; if should_reduce {
1013 let reduction = 1 + ((moves_searched - self.config.lmr_min_moves) / 4) as u32;
1015 let reduced_depth = (depth - 1).saturating_sub(reduction);
1016
1017 let (eval, _) = self.minimax_optimized(&new_board, reduced_depth, -(alpha + 1.0), -alpha, !maximizing, game_phase);
1018 let reduced_eval = -eval;
1019
1020 if reduced_eval > alpha && reduced_eval < beta {
1021 let (eval, _) = self.minimax_optimized(&new_board, depth - 1, -beta, -alpha, !maximizing, game_phase);
1023 -eval
1024 } else {
1025 reduced_eval
1026 }
1027 } else {
1028 if self.config.enable_principal_variation_search {
1030 let (eval, _) = self.minimax_optimized(&new_board, depth - 1, -(alpha + 1.0), -alpha, !maximizing, game_phase);
1032 let scout_eval = -eval;
1033
1034 if scout_eval > alpha && scout_eval < beta {
1035 let (eval, _) = self.minimax_optimized(&new_board, depth - 1, -beta, -alpha, !maximizing, game_phase);
1037 -eval
1038 } else {
1039 scout_eval
1040 }
1041 } else {
1042 let (eval, _) = self.minimax_optimized(&new_board, depth - 1, -beta, -alpha, !maximizing, game_phase);
1044 -eval
1045 }
1046 }
1047 };
1048
1049 if maximizing {
1050 if evaluation > best_evaluation {
1051 best_evaluation = evaluation;
1052 best_move = Some(mv);
1053 }
1054 alpha = alpha.max(evaluation);
1055 } else {
1056 if evaluation < best_evaluation {
1057 best_evaluation = evaluation;
1058 best_move = Some(mv);
1059 }
1060 alpha = alpha.min(evaluation);
1061 }
1062
1063 if alpha >= beta {
1065 self.update_killer_moves(mv, depth as usize);
1067 self.update_history_heuristic(mv, depth);
1068 break;
1069 }
1070 }
1071
1072 let node_type = if best_evaluation <= original_alpha {
1074 NodeType::UpperBound
1075 } else if best_evaluation >= beta {
1076 NodeType::LowerBound
1077 } else {
1078 NodeType::Exact
1079 };
1080
1081 let entry = TranspositionEntry {
1082 depth,
1083 evaluation: best_evaluation,
1084 best_move,
1085 node_type,
1086 age: 0,
1087 };
1088 self.transposition_table.insert(hash, entry);
1089
1090 (best_evaluation, best_move)
1091 }
1092
1093 fn quiescence_search_optimized(&mut self, board: &Board, mut alpha: f32, beta: f32, maximizing: bool, depth: u32) -> f32 {
1095 self.nodes_searched += 1;
1096
1097 if depth == 0 {
1098 return self.evaluate_position_optimized(board, self.detect_game_phase(board));
1099 }
1100
1101 let stand_pat = self.evaluate_position_optimized(board, self.detect_game_phase(board));
1102
1103 if maximizing {
1104 alpha = alpha.max(stand_pat);
1105 if alpha >= beta {
1106 return beta;
1107 }
1108 } else {
1109 alpha = alpha.min(stand_pat);
1110 if alpha <= beta {
1111 return beta;
1112 }
1113 }
1114
1115 let moves: Vec<ChessMove> = MoveGen::new_legal(board)
1117 .filter(|mv| {
1118 board.piece_on(mv.get_dest()).is_some() || mv.get_promotion().is_some() || {
1121 let new_board = board.make_move_new(*mv);
1122 new_board.checkers().popcnt() > 0 }
1124 })
1125 .collect();
1126
1127 if moves.is_empty() {
1128 return stand_pat;
1129 }
1130
1131 for mv in moves {
1132 let new_board = board.make_move_new(mv);
1133 let evaluation = self.quiescence_search_optimized(&new_board, -beta, -alpha, !maximizing, depth - 1);
1134 let evaluation = -evaluation;
1135
1136 if maximizing {
1137 alpha = alpha.max(evaluation);
1138 } else {
1139 alpha = alpha.min(evaluation);
1140 }
1141
1142 if alpha >= beta {
1143 break;
1144 }
1145 }
1146
1147 alpha
1148 }
1149
1150 fn evaluate_position_optimized(&self, board: &Board, game_phase: GamePhase) -> f32 {
1152 let mut white_material = 0.0;
1154 let mut black_material = 0.0;
1155
1156 for square in chess::ALL_SQUARES {
1157 if let Some(piece) = board.piece_on(square) {
1158 let value = match piece {
1159 chess::Piece::Pawn => 100.0,
1160 chess::Piece::Knight => 300.0,
1161 chess::Piece::Bishop => 320.0,
1162 chess::Piece::Rook => 500.0,
1163 chess::Piece::Queen => 900.0,
1164 chess::Piece::King => 0.0,
1165 };
1166
1167 if board.color_on(square) == Some(Color::White) {
1168 white_material += value;
1169 } else {
1170 black_material += value;
1171 }
1172 }
1173 }
1174
1175 let material_balance = white_material - black_material;
1176
1177 let phase_adjustment = match game_phase {
1179 GamePhase::Opening => {
1180 let mut adjustment = 0.0;
1182
1183 adjustment += 0.0;
1185
1186 adjustment
1187 },
1188 GamePhase::Middlegame => {
1189 0.0
1191 },
1192 GamePhase::Endgame => {
1193 let mut adjustment = 0.0;
1195
1196 adjustment += 0.0;
1198 adjustment
1199 },
1200 };
1201
1202 material_balance + phase_adjustment
1203 }
1204
1205
1206 fn should_stop_search(&self) -> bool {
1208 self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128
1209 || self.nodes_searched > self.config.max_nodes
1210 }
1211
1212 fn update_killer_moves(&mut self, mv: ChessMove, depth: usize) {
1214 if depth < self.killer_moves.len() {
1215 if self.killer_moves[depth][0] != Some(mv) {
1217 self.killer_moves[depth][1] = self.killer_moves[depth][0];
1218 self.killer_moves[depth][0] = Some(mv);
1219 }
1220 }
1221 }
1222
1223 fn update_history_heuristic(&mut self, mv: ChessMove, depth: u32) {
1225 let key = (mv.get_source(), mv.get_dest());
1226 let bonus = depth * depth; *self.history_heuristic.entry(key).or_insert(0) += bonus;
1228
1229 if let Some(score) = self.history_heuristic.get_mut(&key) {
1231 *score = (*score).min(10000);
1232 }
1233 }
1234
1235 pub fn search(&mut self, board: &Board) -> TacticalResult {
1237 self.nodes_searched = 0;
1238 self.start_time = Instant::now();
1239 self.transposition_table.clear();
1240
1241 let is_tactical = self.is_tactical_position(board);
1243
1244 let (search_time_ms, search_depth) = if false {
1246 self.calculate_dynamic_search_limits(board)
1248 } else {
1249 (self.config.max_time_ms, self.config.max_depth)
1250 };
1251
1252 let original_time = self.config.max_time_ms;
1254 let original_depth = self.config.max_depth;
1255 self.config.max_time_ms = search_time_ms;
1256 self.config.max_depth = search_depth;
1257
1258 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
1259 self.iterative_deepening_search(board)
1260 } else {
1261 let (eval, mv) = self.minimax(
1262 board,
1263 search_depth,
1264 f32::NEG_INFINITY,
1265 f32::INFINITY,
1266 board.side_to_move() == Color::White,
1267 );
1268 (eval, mv, search_depth)
1269 };
1270
1271 self.config.max_time_ms = original_time;
1273 self.config.max_depth = original_depth;
1274
1275 TacticalResult {
1276 evaluation,
1277 best_move,
1278 depth_reached,
1279 nodes_searched: self.nodes_searched,
1280 time_elapsed: self.start_time.elapsed(),
1281 is_tactical,
1282 }
1283 }
1284
1285 pub fn search_parallel(&mut self, board: &Board) -> TacticalResult {
1287 if !self.config.enable_parallel_search || self.config.num_threads <= 1 {
1288 return self.search(board); }
1290
1291 self.nodes_searched = 0;
1292 self.start_time = Instant::now();
1293 self.transposition_table.clear();
1294
1295 let is_tactical = self.is_tactical_position(board);
1296 let moves = self.generate_ordered_moves(board);
1297
1298 if moves.is_empty() {
1299 return TacticalResult {
1300 evaluation: self.evaluate_terminal_position(board),
1301 best_move: None,
1302 depth_reached: 1,
1303 nodes_searched: 1,
1304 time_elapsed: self.start_time.elapsed(),
1305 is_tactical,
1306 };
1307 }
1308
1309 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
1311 self.parallel_iterative_deepening(board, moves)
1312 } else {
1313 self.parallel_root_search(board, moves, self.config.max_depth)
1314 };
1315
1316 TacticalResult {
1317 evaluation,
1318 best_move,
1319 depth_reached,
1320 nodes_searched: self.nodes_searched,
1321 time_elapsed: self.start_time.elapsed(),
1322 is_tactical,
1323 }
1324 }
1325
1326 fn parallel_root_search(
1328 &mut self,
1329 board: &Board,
1330 moves: Vec<ChessMove>,
1331 depth: u32,
1332 ) -> (f32, Option<ChessMove>, u32) {
1333 let maximizing = board.side_to_move() == Color::White;
1334 let nodes_counter = Arc::new(Mutex::new(0u64));
1335
1336 let move_scores: Vec<(ChessMove, f32)> = moves
1338 .into_par_iter()
1339 .map(|mv| {
1340 let new_board = board.make_move_new(mv);
1341 let mut search_clone = self.clone();
1342 search_clone.nodes_searched = 0;
1343
1344 let (eval, _) = search_clone.minimax(
1345 &new_board,
1346 depth - 1,
1347 f32::NEG_INFINITY,
1348 f32::INFINITY,
1349 !maximizing,
1350 );
1351
1352 if let Ok(mut counter) = nodes_counter.lock() {
1354 *counter += search_clone.nodes_searched;
1355 }
1356
1357 (mv, -eval)
1359 })
1360 .collect();
1361
1362 if let Ok(counter) = nodes_counter.lock() {
1364 self.nodes_searched = *counter;
1365 }
1366
1367 let best = move_scores
1369 .into_iter()
1370 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1371
1372 match best {
1373 Some((best_move, best_eval)) => (best_eval, Some(best_move), depth),
1374 None => (0.0, None, depth),
1375 }
1376 }
1377
1378 fn parallel_iterative_deepening(
1380 &mut self,
1381 board: &Board,
1382 mut moves: Vec<ChessMove>,
1383 ) -> (f32, Option<ChessMove>, u32) {
1384 let mut best_move: Option<ChessMove> = None;
1385 let mut best_evaluation = 0.0;
1386 let mut completed_depth = 0;
1387
1388 for depth in 1..=self.config.max_depth {
1389 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128 {
1391 break;
1392 }
1393
1394 let (eval, mv, _) = self.parallel_root_search(board, moves.clone(), depth);
1395
1396 best_evaluation = eval;
1397 best_move = mv;
1398 completed_depth = depth;
1399
1400 if let Some(best) = best_move {
1402 if let Some(pos) = moves.iter().position(|&m| m == best) {
1403 moves.swap(0, pos);
1404 }
1405 }
1406 }
1407
1408 (best_evaluation, best_move, completed_depth)
1409 }
1410
1411 fn iterative_deepening_search(&mut self, board: &Board) -> (f32, Option<ChessMove>, u32) {
1413 let mut best_move: Option<ChessMove> = None;
1414 let mut best_evaluation = 0.0;
1415 let mut completed_depth = 0;
1416
1417 for depth in 1..=self.config.max_depth {
1422 let depth_start_time = std::time::Instant::now();
1423
1424 let elapsed = self.start_time.elapsed().as_millis() as u64;
1426 let time_remaining = self.config.max_time_ms.saturating_sub(elapsed);
1427
1428 if time_remaining < (self.config.max_time_ms / 10) {
1431 break;
1433 }
1434
1435 let window_size = if self.config.enable_aspiration_windows && depth > 2 {
1436 50.0 } else {
1438 f32::INFINITY
1439 };
1440
1441 let (evaluation, mv) = if self.config.enable_aspiration_windows && depth > 2 {
1442 self.aspiration_window_search(board, depth, best_evaluation, window_size)
1443 } else {
1444 self.minimax(
1445 board,
1446 depth,
1447 f32::NEG_INFINITY,
1448 f32::INFINITY,
1449 board.side_to_move() == Color::White,
1450 )
1451 };
1452
1453 if mv.is_some() {
1456 best_evaluation = evaluation;
1457 best_move = mv;
1458 }
1459 completed_depth = depth;
1460
1461 if evaluation.abs() > 9000.0 {
1463 break;
1464 }
1465
1466 if depth >= 8 && mv.is_some() && evaluation.abs() >= 5.0 {
1469 break;
1470 }
1471
1472 let depth_time_taken = depth_start_time.elapsed().as_millis() as u64;
1475 let remaining_time = self
1476 .config
1477 .max_time_ms
1478 .saturating_sub(elapsed + depth_time_taken);
1479
1480 if depth < self.config.max_depth {
1482 let estimated_next_depth_time = depth_time_taken * 3; if estimated_next_depth_time > remaining_time {
1484 break;
1485 }
1486 }
1487 }
1488
1489 (best_evaluation, best_move, completed_depth)
1490 }
1491
1492 fn calculate_position_complexity(&self, board: &Board) -> f32 {
1494 let mut complexity = 0.0;
1495
1496 let total_pieces = board.combined().popcnt() as f32;
1498 complexity += (total_pieces - 20.0) / 12.0; let legal_moves = MoveGen::new_legal(board).count() as f32;
1502 complexity += (legal_moves - 20.0) / 20.0; if board.checkers().popcnt() > 0 {
1506 complexity += 0.5;
1507 }
1508
1509 if self.is_tactical_position(board) {
1511 complexity += 0.3;
1512 }
1513
1514 let game_phase = self.determine_game_phase(board);
1516 if game_phase == GamePhase::Endgame {
1517 complexity -= 0.3;
1518 }
1519
1520 complexity.clamp(0.2, 1.5)
1522 }
1523
1524 fn aspiration_window_search(
1526 &mut self,
1527 board: &Board,
1528 depth: u32,
1529 prev_score: f32,
1530 window: f32,
1531 ) -> (f32, Option<ChessMove>) {
1532 let mut alpha = prev_score - window;
1533 let mut beta = prev_score + window;
1534
1535 loop {
1536 let (score, mv) = self.minimax(
1537 board,
1538 depth,
1539 alpha,
1540 beta,
1541 board.side_to_move() == Color::White,
1542 );
1543
1544 if score <= alpha {
1545 alpha = f32::NEG_INFINITY;
1547 } else if score >= beta {
1548 beta = f32::INFINITY;
1550 } else {
1551 return (score, mv);
1553 }
1554 }
1555 }
1556
1557 fn minimax(
1559 &mut self,
1560 board: &Board,
1561 depth: u32,
1562 alpha: f32,
1563 beta: f32,
1564 maximizing: bool,
1565 ) -> (f32, Option<ChessMove>) {
1566 self.minimax_with_extensions(board, depth, alpha, beta, maximizing, 0)
1567 }
1568
1569 fn minimax_with_extensions(
1570 &mut self,
1571 board: &Board,
1572 depth: u32,
1573 alpha: f32,
1574 beta: f32,
1575 maximizing: bool,
1576 extensions_used: u32,
1577 ) -> (f32, Option<ChessMove>) {
1578 self.nodes_searched += 1;
1579
1580 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128
1582 || self.nodes_searched > self.config.max_nodes
1583 {
1584 return (self.evaluate_position(board), None);
1585 }
1586
1587 let mut actual_depth = depth;
1589 if self.config.enable_check_extensions
1590 && board.checkers().popcnt() > 0
1591 && extensions_used < self.config.max_extensions_per_line
1592 {
1593 actual_depth += self.config.check_extension_depth;
1594 }
1595
1596 let mut new_extensions_used = extensions_used;
1598 if self.config.enable_check_extensions
1599 && depth <= 2
1600 && extensions_used < 2
1601 && self.has_tactical_threats(board)
1602 {
1603 actual_depth += 1; new_extensions_used += 1;
1605 }
1606
1607 if actual_depth == 0 {
1609 return if self.config.enable_quiescence {
1613 (
1614 self.quiescence_search(
1615 board,
1616 self.config.quiescence_depth,
1617 alpha,
1618 beta,
1619 maximizing,
1620 ),
1621 None,
1622 )
1623 } else {
1624 (self.evaluate_position(board), None)
1625 };
1626 }
1627
1628 if board.status() != chess::BoardStatus::Ongoing {
1629 return (self.evaluate_terminal_position(board), None);
1630 }
1631
1632 if self.config.enable_transposition_table {
1634 if let Some(entry) = self.transposition_table.get(board.get_hash()) {
1635 if entry.depth >= depth {
1636 match entry.node_type {
1637 NodeType::Exact => return (entry.evaluation, entry.best_move),
1638 NodeType::LowerBound if entry.evaluation >= beta => {
1639 return (entry.evaluation, entry.best_move)
1640 }
1641 NodeType::UpperBound if entry.evaluation <= alpha => {
1642 return (entry.evaluation, entry.best_move)
1643 }
1644 _ => {}
1645 }
1646 }
1647 }
1648 }
1649
1650 let static_eval = self.evaluate_position(board);
1652
1653 if self.config.enable_razoring
1655 && (1..=3).contains(&depth)
1656 && !maximizing && static_eval + self.config.razor_margin < alpha
1658 {
1659 let razor_eval = self.quiescence_search(board, 1, alpha, beta, maximizing);
1661 if razor_eval < alpha {
1662 return (razor_eval, None);
1663 }
1664 }
1665
1666 if self.config.enable_futility_pruning
1668 && depth <= 4 && !maximizing
1670 && board.checkers().popcnt() == 0
1671 && static_eval + (self.config.futility_margin_base * depth as f32) < alpha
1672 {
1673 return (static_eval, None);
1675 }
1676
1677 if self.config.enable_extended_futility_pruning
1679 && (2..=4).contains(&depth)
1680 && !maximizing
1681 && board.checkers().popcnt() == 0
1682 {
1684 let futility_margin = self.config.extended_futility_margin * (depth as f32);
1685
1686 if static_eval + futility_margin < alpha {
1688 return (static_eval, None);
1689 }
1690
1691 if static_eval + 5.0 < alpha && depth <= 3 {
1693 return (static_eval, None);
1694 }
1695 }
1696
1697 if self.config.enable_reverse_futility_pruning
1699 && depth <= 7 && maximizing
1701 && board.checkers().popcnt() == 0
1702 && static_eval >= beta + self.config.reverse_futility_margin
1703 {
1704 return (static_eval, None);
1706 }
1707
1708 if self.config.enable_static_null_move_pruning
1710 && depth <= 6
1711 && maximizing
1712 && board.checkers().popcnt() == 0
1713 && self.has_non_pawn_material(board, board.side_to_move())
1714 && static_eval >= beta + 200.0
1715 {
1717 return (static_eval, None);
1719 }
1720
1721 if self.config.enable_null_move_pruning
1723 && depth >= 2 && maximizing
1725 && board.checkers().popcnt() == 0
1726 && self.has_non_pawn_material(board, board.side_to_move())
1727 && static_eval >= beta
1728 {
1730 let null_move_reduction = if depth >= 7 {
1732 4 + (depth - 7) / 4 } else if depth >= 4 {
1734 3
1735 } else {
1736 2
1737 };
1738
1739 let new_depth = depth.saturating_sub(null_move_reduction);
1740
1741 let null_board = board.null_move().unwrap_or(*board);
1743 let (null_score, _) = self.minimax_with_extensions(
1744 &null_board,
1745 new_depth,
1746 -beta,
1747 -beta + 1.0,
1748 !maximizing,
1749 new_extensions_used,
1750 );
1751
1752 if null_score >= beta {
1754 if depth >= 12 && null_score < 9000.0 {
1756 let verify_depth = depth.saturating_sub(4);
1758 let (verify_score, _) = self.minimax_with_extensions(
1759 board,
1760 verify_depth,
1761 beta - 1.0,
1762 beta,
1763 maximizing,
1764 new_extensions_used,
1765 );
1766 if verify_score >= beta {
1767 return (beta, None);
1768 }
1769 } else {
1770 return (beta, None);
1771 }
1772 }
1773 }
1774
1775 let hash_move = if self.config.enable_transposition_table {
1777 self.transposition_table
1778 .get(board.get_hash())
1779 .and_then(|entry| entry.best_move)
1780 } else {
1781 None
1782 };
1783
1784 let moves = self.generate_ordered_moves_with_hash(board, hash_move, depth);
1786
1787 let (best_value, best_move) =
1788 if self.config.enable_principal_variation_search && moves.len() > 1 {
1789 self.principal_variation_search(
1791 board,
1792 depth,
1793 alpha,
1794 beta,
1795 maximizing,
1796 moves,
1797 new_extensions_used,
1798 )
1799 } else {
1800 self.alpha_beta_search(
1802 board,
1803 depth,
1804 alpha,
1805 beta,
1806 maximizing,
1807 moves,
1808 new_extensions_used,
1809 )
1810 };
1811
1812 if self.config.enable_transposition_table {
1814 let node_type = if best_value <= alpha {
1815 NodeType::UpperBound
1816 } else if best_value >= beta {
1817 NodeType::LowerBound
1818 } else {
1819 NodeType::Exact
1820 };
1821
1822 self.transposition_table.insert(
1823 board.get_hash(),
1824 TranspositionEntry {
1825 depth,
1826 evaluation: best_value,
1827 best_move,
1828 node_type,
1829 age: 0, },
1831 );
1832 }
1833
1834 (best_value, best_move)
1835 }
1836
1837 fn principal_variation_search(
1839 &mut self,
1840 board: &Board,
1841 depth: u32,
1842 mut alpha: f32,
1843 mut beta: f32,
1844 maximizing: bool,
1845 moves: Vec<ChessMove>,
1846 extensions_used: u32,
1847 ) -> (f32, Option<ChessMove>) {
1848 let mut best_move: Option<ChessMove> = None;
1849 let mut best_value = if maximizing {
1850 f32::NEG_INFINITY
1851 } else {
1852 f32::INFINITY
1853 };
1854 let mut _pv_found = false;
1855 let mut first_move = true;
1856
1857 if moves.is_empty() {
1859 return (self.evaluate_position(board), None);
1860 }
1861
1862 for (move_index, chess_move) in moves.into_iter().enumerate() {
1863 let new_board = board.make_move_new(chess_move);
1864 let mut evaluation;
1865
1866 if self.config.enable_move_count_pruning
1868 && depth <= 5
1869 && move_index
1870 >= (self.config.move_count_base as usize
1871 + (depth as f32 * self.config.move_count_depth_factor) as usize)
1872 && !self.is_capture_or_promotion(&chess_move, board)
1873 && new_board.checkers().popcnt() == 0
1874 && !self.is_killer_move(&chess_move)
1875 && best_move.is_some()
1876 {
1878 continue; }
1880
1881 if self.config.enable_history_pruning
1883 && depth <= 4
1884 && move_index >= 4
1885 && !self.is_capture_or_promotion(&chess_move, board)
1886 && new_board.checkers().popcnt() == 0
1887 && (self.get_history_score(&chess_move) as i32)
1888 < self.config.history_pruning_threshold
1889 {
1890 continue; }
1892
1893 let reduction = if self.config.enable_late_move_reductions
1895 && depth >= 3 && move_index >= 3 && !self.is_capture_or_promotion(&chess_move, board)
1898 && new_board.checkers().popcnt() == 0
1899 && !self.is_killer_move(&chess_move)
1900 {
1901 let base_reduction = match move_index {
1903 3..=6 => 1,
1904 7..=12 => 2,
1905 13..=20 => 3,
1906 _ => 4, };
1908
1909 let depth_bonus = if depth >= 10 { 1 } else { 0 };
1911
1912 (base_reduction + depth_bonus).min(depth.saturating_sub(1))
1913 } else {
1914 0
1915 };
1916
1917 let search_depth = if depth > reduction {
1918 depth - 1 - reduction
1919 } else {
1920 0
1921 };
1922
1923 if move_index == 0 {
1924 let search_depth = if depth > 0 { depth - 1 } else { 0 };
1926 let (eval, _) = self.minimax_with_extensions(
1927 &new_board,
1928 search_depth,
1929 alpha,
1930 beta,
1931 !maximizing,
1932 extensions_used,
1933 );
1934 evaluation = eval;
1935 _pv_found = true;
1936 } else {
1937 let null_window_alpha = if maximizing { alpha } else { beta - 1.0 };
1939 let null_window_beta = if maximizing { alpha + 1.0 } else { beta };
1940
1941 let (null_eval, _) = self.minimax_with_extensions(
1942 &new_board,
1943 search_depth,
1944 null_window_alpha,
1945 null_window_beta,
1946 !maximizing,
1947 extensions_used,
1948 );
1949
1950 if null_eval > alpha && null_eval < beta {
1952 let full_depth = if reduction > 0 {
1954 if depth > 0 {
1955 depth - 1
1956 } else {
1957 0
1958 }
1959 } else {
1960 search_depth
1961 };
1962 let (full_eval, _) = self.minimax_with_extensions(
1963 &new_board,
1964 full_depth,
1965 alpha,
1966 beta,
1967 !maximizing,
1968 extensions_used,
1969 );
1970 evaluation = full_eval;
1971 } else {
1972 evaluation = null_eval;
1973
1974 if reduction > 0
1976 && ((maximizing && evaluation > alpha)
1977 || (!maximizing && evaluation < beta))
1978 {
1979 let search_depth = if depth > 0 { depth - 1 } else { 0 };
1980 let (re_eval, _) = self.minimax_with_extensions(
1981 &new_board,
1982 search_depth,
1983 alpha,
1984 beta,
1985 !maximizing,
1986 extensions_used,
1987 );
1988 evaluation = re_eval;
1989 }
1990 }
1991 }
1992
1993 if maximizing {
1995 if first_move || evaluation > best_value {
1996 best_value = evaluation;
1997 best_move = Some(chess_move);
1998 }
1999 alpha = alpha.max(evaluation);
2000 } else {
2001 if first_move || evaluation < best_value {
2002 best_value = evaluation;
2003 best_move = Some(chess_move);
2004 }
2005 beta = beta.min(evaluation);
2006 }
2007
2008 first_move = false;
2009
2010 if beta <= alpha {
2012 if !self.is_capture_or_promotion(&chess_move, board) {
2014 self.store_killer_move(chess_move, depth);
2015 self.update_history(&chess_move, depth);
2016 }
2017 break;
2018 }
2019 }
2020
2021 (best_value, best_move)
2022 }
2023
2024 fn is_obvious_blunder(&self, board: &Board, chess_move: ChessMove) -> bool {
2026 let dest = chess_move.get_dest();
2027
2028 let moving_piece = match board.piece_on(chess_move.get_source()) {
2030 Some(piece) => piece,
2031 None => return false, };
2033
2034 let captured_value = board.piece_on(dest).map_or(0.0, |piece| match piece {
2036 chess::Piece::Pawn => 1.0,
2037 chess::Piece::Knight => 3.0,
2038 chess::Piece::Bishop => 3.0,
2039 chess::Piece::Rook => 5.0,
2040 chess::Piece::Queen => 9.0,
2041 chess::Piece::King => 100.0,
2042 });
2043
2044 let moving_piece_value = match moving_piece {
2045 chess::Piece::Pawn => 1.0,
2046 chess::Piece::Knight => 3.0,
2047 chess::Piece::Bishop => 3.0,
2048 chess::Piece::Rook => 5.0,
2049 chess::Piece::Queen => 9.0,
2050 chess::Piece::King => 0.0, };
2052
2053 let net_exchange = if captured_value > 0.0 {
2058 let see_result = self.calculate_material_exchange(
2060 &chess_move,
2061 board,
2062 board.piece_on(dest).unwrap(),
2063 moving_piece,
2064 ) as f32
2065 / 100.0;
2066
2067 if see_result >= 0.0 {
2069 return false; }
2071 see_result
2072 } else {
2073 let new_board = board.make_move_new(chess_move);
2075 let attackers = self.count_attackers(&new_board, dest, !board.side_to_move());
2076 if attackers > 0 {
2077 let defenders = self.count_attackers(&new_board, dest, board.side_to_move());
2079 if defenders == 0 {
2080 -moving_piece_value } else {
2082 if attackers > defenders {
2084 -moving_piece_value * 0.5 } else {
2086 0.0 }
2088 }
2089 } else {
2090 0.0 }
2092 };
2093
2094 if net_exchange < -0.5 {
2095 let new_board = board.make_move_new(chess_move);
2098
2099 let is_check = new_board.checkers().popcnt() > 0;
2104 let near_enemy_king = self.is_near_enemy_king(&new_board, dest);
2105 let catastrophic_loss = net_exchange < -3.0;
2106
2107 if catastrophic_loss || !near_enemy_king || (!is_check && net_exchange < -1.0) {
2108 return true; }
2110 }
2111
2112 if moving_piece == chess::Piece::Queen && captured_value == 0.0 {
2114 let new_board = board.make_move_new(chess_move);
2117 if self.is_likely_under_attack(&new_board, dest, chess::Piece::Queen) {
2118 return true;
2119 }
2120 }
2121
2122 false
2123 }
2124
2125 fn is_near_enemy_king(&self, board: &Board, square: Square) -> bool {
2127 let enemy_color = board.side_to_move();
2128 let enemy_king_square =
2129 (board.pieces(chess::Piece::King) & board.color_combined(enemy_color)).to_square();
2130
2131 let rank_diff = (square.get_rank().to_index() as i8
2133 - enemy_king_square.get_rank().to_index() as i8)
2134 .abs();
2135 let file_diff = (square.get_file().to_index() as i8
2136 - enemy_king_square.get_file().to_index() as i8)
2137 .abs();
2138
2139 rank_diff <= 2 && file_diff <= 2
2140 }
2141
2142 fn is_likely_under_attack(&self, board: &Board, square: Square, piece: chess::Piece) -> bool {
2144 if piece == chess::Piece::Queen {
2146 let total_pieces = board.combined().popcnt();
2148 if total_pieces > 28 {
2149 let file = square.get_file();
2152 if file == chess::File::D || file == chess::File::E {
2153 return true;
2154 }
2155 }
2156 }
2157
2158 false
2159 }
2160
2161 fn alpha_beta_search(
2163 &mut self,
2164 board: &Board,
2165 depth: u32,
2166 mut alpha: f32,
2167 mut beta: f32,
2168 maximizing: bool,
2169 moves: Vec<ChessMove>,
2170 extensions_used: u32,
2171 ) -> (f32, Option<ChessMove>) {
2172 let mut best_move: Option<ChessMove> = None;
2173 let mut best_value = if maximizing {
2174 f32::NEG_INFINITY
2175 } else {
2176 f32::INFINITY
2177 };
2178 let mut first_move = true;
2179
2180 if moves.is_empty() {
2182 return (self.evaluate_position(board), None);
2183 }
2184
2185 for (move_index, chess_move) in moves.into_iter().enumerate() {
2186 let new_board = board.make_move_new(chess_move);
2187
2188 let reduction = if self.config.enable_late_move_reductions
2190 && depth >= 3 && move_index >= 3 && !self.is_capture_or_promotion(&chess_move, board)
2193 && new_board.checkers().popcnt() == 0
2194 && !self.is_killer_move(&chess_move)
2195 {
2196 let base_reduction = match move_index {
2198 3..=6 => 1,
2199 7..=12 => 2,
2200 13..=20 => 3,
2201 _ => 4, };
2203
2204 let depth_bonus = if depth >= 10 { 1 } else { 0 };
2206
2207 (base_reduction + depth_bonus).min(depth.saturating_sub(1))
2208 } else {
2209 0
2210 };
2211
2212 let search_depth = if depth > reduction {
2213 depth - 1 - reduction
2214 } else {
2215 0
2216 };
2217
2218 let (evaluation, _) = self.minimax_with_extensions(
2219 &new_board,
2220 search_depth,
2221 alpha,
2222 beta,
2223 !maximizing,
2224 extensions_used,
2225 );
2226
2227 let final_evaluation = if reduction > 0
2229 && ((maximizing && evaluation > alpha) || (!maximizing && evaluation < beta))
2230 {
2231 let search_depth = if depth > 0 { depth - 1 } else { 0 };
2232 let (re_eval, _) = self.minimax_with_extensions(
2233 &new_board,
2234 search_depth,
2235 alpha,
2236 beta,
2237 !maximizing,
2238 extensions_used,
2239 );
2240 re_eval
2241 } else {
2242 evaluation
2243 };
2244
2245 if maximizing {
2246 if first_move || final_evaluation > best_value {
2247 best_value = final_evaluation;
2248 best_move = Some(chess_move);
2249 }
2250 alpha = alpha.max(final_evaluation);
2251 } else {
2252 if first_move || final_evaluation < best_value {
2253 best_value = final_evaluation;
2254 best_move = Some(chess_move);
2255 }
2256 beta = beta.min(final_evaluation);
2257 }
2258
2259 first_move = false;
2260
2261 if beta <= alpha {
2263 if !self.is_capture_or_promotion(&chess_move, board) {
2265 self.store_killer_move(chess_move, depth);
2266 self.update_history(&chess_move, depth);
2267 }
2268 break;
2269 }
2270 }
2271
2272 (best_value, best_move)
2273 }
2274
2275 fn quiescence_search(
2277 &mut self,
2278 board: &Board,
2279 depth: u32,
2280 mut alpha: f32,
2281 beta: f32,
2282 maximizing: bool,
2283 ) -> f32 {
2284 self.nodes_searched += 1;
2285
2286 let stand_pat = self.evaluate_position(board);
2287
2288 if depth == 0 {
2289 return stand_pat;
2290 }
2291
2292 if maximizing {
2293 if stand_pat >= beta {
2294 return beta;
2295 }
2296 alpha = alpha.max(stand_pat);
2297
2298 if stand_pat + 9.0 < alpha {
2300 return stand_pat;
2302 }
2303 } else {
2304 if stand_pat <= alpha {
2305 return alpha;
2306 }
2307
2308 if stand_pat - 9.0 > alpha {
2310 return stand_pat;
2312 }
2313 }
2314
2315 let captures_and_checks = self.generate_captures_and_checks(board);
2317
2318 for chess_move in captures_and_checks {
2319 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
2321 if !self.is_good_capture(&chess_move, board, captured_piece) {
2322 continue;
2324 }
2325 }
2326
2327 let new_board = board.make_move_new(chess_move);
2328
2329 if new_board.status() == chess::BoardStatus::Checkmate {
2331 let mate_score = if maximizing { 9999.0 } else { -9999.0 };
2332 if maximizing {
2333 alpha = alpha.max(mate_score);
2334 if alpha >= beta {
2335 return beta;
2336 }
2337 } else {
2338 if mate_score <= alpha {
2339 return alpha;
2340 }
2341 }
2342 continue;
2343 }
2344
2345 let evaluation =
2346 self.quiescence_search(&new_board, depth - 1, alpha, beta, !maximizing);
2347
2348 if maximizing {
2349 alpha = alpha.max(evaluation);
2350 if alpha >= beta {
2351 break;
2352 }
2353 } else if evaluation <= alpha {
2354 return alpha;
2355 }
2356 }
2357
2358 stand_pat
2359 }
2360
2361 fn generate_ordered_moves(&self, board: &Board) -> Vec<ChessMove> {
2363 self.generate_ordered_moves_with_hash(board, None, 1) }
2365
2366 fn generate_ordered_moves_with_hash(
2368 &self,
2369 board: &Board,
2370 hash_move: Option<ChessMove>,
2371 depth: u32,
2372 ) -> Vec<ChessMove> {
2373 let mut moves: Vec<_> = MoveGen::new_legal(board).collect();
2374
2375 moves.sort_by(|a, b| {
2377 let a_score = self.get_move_order_score(a, board, hash_move, depth);
2378 let b_score = self.get_move_order_score(b, board, hash_move, depth);
2379 b_score.cmp(&a_score) });
2381
2382 moves
2383 }
2384
2385 fn get_move_order_score(
2387 &self,
2388 chess_move: &ChessMove,
2389 board: &Board,
2390 hash_move: Option<ChessMove>,
2391 depth: u32,
2392 ) -> i32 {
2393 if let Some(hash) = hash_move {
2401 if hash == *chess_move {
2402 return 1_000_000; }
2404 }
2405
2406 if let Some(_captured_piece) = board.piece_on(chess_move.get_dest()) {
2408 let mvv_lva_score = self.mvv_lva_score(chess_move, board);
2409
2410 return 900_000 + mvv_lva_score; }
2415
2416 if chess_move.get_promotion().is_some() {
2418 let promotion_piece = chess_move.get_promotion().unwrap();
2419 let promotion_value = match promotion_piece {
2420 chess::Piece::Queen => 800_000,
2421 chess::Piece::Rook => 700_000,
2422 chess::Piece::Bishop => 600_000,
2423 chess::Piece::Knight => 590_000,
2424 _ => 500_000,
2425 };
2426 return promotion_value;
2427 }
2428
2429 let tactical_bonus = self.evaluate_tactical_move_bonus(chess_move, board);
2431 if tactical_bonus > 0 {
2432 return 550_000 + tactical_bonus; }
2434
2435 if self.is_killer_move_at_depth(chess_move, depth) {
2437 return 500_000;
2438 }
2439
2440 if self.is_counter_move(chess_move) {
2442 return 400_000;
2443 }
2444
2445 if self.is_castling_move(chess_move, board) {
2447 return 250_000; }
2449
2450 if self.gives_check(chess_move, board) {
2452 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
2454 if let Some(attacker_piece) = board.piece_on(chess_move.get_source()) {
2456 let victim_value = self.get_piece_value(captured_piece);
2457 let attacker_value = self.get_piece_value(attacker_piece);
2458 if victim_value < attacker_value {
2459 return 5_000; }
2462 }
2463 return 300_000; } else {
2465 if let Some(_moving_piece) = board.piece_on(chess_move.get_source()) {
2468 let mut temp_board = *board;
2470 temp_board = temp_board.make_move_new(*chess_move);
2471
2472 let attackers = self.count_attackers(
2474 &temp_board,
2475 chess_move.get_dest(),
2476 !temp_board.side_to_move(),
2477 );
2478 let defenders = self.count_attackers(
2479 &temp_board,
2480 chess_move.get_dest(),
2481 temp_board.side_to_move(),
2482 );
2483
2484 if attackers > defenders {
2485 return 8_000;
2487 }
2488 }
2489 return 50_000; }
2491 }
2492
2493 let history_score = self.get_history_score(chess_move);
2495 200_000 + history_score as i32 }
2497
2498 pub fn is_good_capture(
2500 &self,
2501 chess_move: &ChessMove,
2502 board: &Board,
2503 captured_piece: chess::Piece,
2504 ) -> bool {
2505 let attacker_piece = board.piece_on(chess_move.get_source());
2506 if attacker_piece.is_none() {
2507 return false;
2508 }
2509
2510 let attacker_value = self.get_piece_value(attacker_piece.unwrap());
2511 let victim_value = self.get_piece_value(captured_piece);
2512
2513 let immediate_gain = victim_value - attacker_value;
2515
2516 if immediate_gain > 0 {
2518 return true;
2519 }
2520
2521 if immediate_gain < 0 {
2523 let defenders =
2525 self.count_attackers(board, chess_move.get_dest(), !board.side_to_move());
2526
2527 if defenders > 0 {
2529 return false;
2530 }
2531
2532 return immediate_gain >= -100; }
2535
2536 true
2538 }
2539
2540 fn get_piece_value(&self, piece: chess::Piece) -> i32 {
2542 match piece {
2544 chess::Piece::Pawn => 100,
2545 chess::Piece::Knight => 320,
2546 chess::Piece::Bishop => 330,
2547 chess::Piece::Rook => 500,
2548 chess::Piece::Queen => 900,
2549 chess::Piece::King => 10000,
2550 }
2551 }
2552
2553 pub fn calculate_material_exchange(
2556 &self,
2557 chess_move: &ChessMove,
2558 board: &Board,
2559 captured_piece: chess::Piece,
2560 attacker_piece: chess::Piece,
2561 ) -> i32 {
2562 let dest_square = chess_move.get_dest();
2563
2564 let mut white_attackers =
2566 self.get_all_attackers_of_square(board, dest_square, chess::Color::White);
2567 let mut black_attackers =
2568 self.get_all_attackers_of_square(board, dest_square, chess::Color::Black);
2569
2570 if board.side_to_move() == chess::Color::White {
2572 if let Some(pos) = white_attackers.iter().position(|&p| p == attacker_piece) {
2573 white_attackers.remove(pos);
2574 }
2575 } else {
2576 if let Some(pos) = black_attackers.iter().position(|&p| p == attacker_piece) {
2577 black_attackers.remove(pos);
2578 }
2579 }
2580
2581 white_attackers.sort_by_key(|&piece| self.get_piece_value(piece));
2583 black_attackers.sort_by_key(|&piece| self.get_piece_value(piece));
2584
2585 let mut gains = vec![self.get_piece_value(captured_piece)];
2587 let mut current_attacker_value = self.get_piece_value(attacker_piece);
2588 let mut to_move = !board.side_to_move();
2589
2590 loop {
2592 let attackers = if to_move == chess::Color::White {
2593 &mut white_attackers
2594 } else {
2595 &mut black_attackers
2596 };
2597
2598 if attackers.is_empty() {
2599 break; }
2601
2602 let next_attacker = attackers.remove(0);
2604 let next_attacker_value = self.get_piece_value(next_attacker);
2605
2606 gains.push(current_attacker_value);
2608 current_attacker_value = next_attacker_value;
2609 to_move = !to_move;
2610 }
2611
2612 while gains.len() > 1 {
2615 let last_gain = gains.pop().unwrap();
2616 let gains_len = gains.len();
2617 let prev_gain = gains.last_mut().unwrap();
2618
2619 if gains_len % 2 == 1 {
2623 *prev_gain = (*prev_gain).max(*prev_gain - last_gain);
2625 } else {
2626 *prev_gain = (*prev_gain).min(*prev_gain - last_gain);
2628 }
2629 }
2630
2631 gains[0]
2632 }
2633
2634 pub fn get_all_attackers_of_square(
2636 &self,
2637 board: &Board,
2638 square: chess::Square,
2639 color: chess::Color,
2640 ) -> Vec<chess::Piece> {
2641 let mut attackers = Vec::new();
2642
2643 for piece_type in [
2645 chess::Piece::Pawn,
2646 chess::Piece::Knight,
2647 chess::Piece::Bishop,
2648 chess::Piece::Rook,
2649 chess::Piece::Queen,
2650 chess::Piece::King,
2651 ] {
2652 let pieces = board.pieces(piece_type) & board.color_combined(color);
2653
2654 for piece_square in pieces {
2655 if self.piece_can_attack_square(board, piece_square, square, piece_type) {
2656 attackers.push(piece_type);
2657 }
2658 }
2659 }
2660
2661 attackers
2662 }
2663
2664 fn piece_can_attack_square(
2666 &self,
2667 board: &Board,
2668 piece_square: chess::Square,
2669 target_square: chess::Square,
2670 piece_type: chess::Piece,
2671 ) -> bool {
2672 if piece_square == target_square {
2673 return false; }
2675
2676 match piece_type {
2677 chess::Piece::Pawn => {
2678 let color = board.color_on(piece_square).unwrap_or(chess::Color::White);
2679 let source_rank = piece_square.get_rank().to_index() as i32;
2680 let source_file = piece_square.get_file().to_index() as i32;
2681 let target_rank = target_square.get_rank().to_index() as i32;
2682 let target_file = target_square.get_file().to_index() as i32;
2683
2684 let rank_diff = target_rank - source_rank;
2685 let file_diff = (target_file - source_file).abs();
2686
2687 if color == chess::Color::White {
2688 rank_diff == 1 && file_diff == 1 } else {
2690 rank_diff == -1 && file_diff == 1 }
2692 }
2693 chess::Piece::Knight => {
2694 let source_rank = piece_square.get_rank().to_index() as i32;
2695 let source_file = piece_square.get_file().to_index() as i32;
2696 let target_rank = target_square.get_rank().to_index() as i32;
2697 let target_file = target_square.get_file().to_index() as i32;
2698
2699 let rank_diff = (target_rank - source_rank).abs();
2700 let file_diff = (target_file - source_file).abs();
2701
2702 (rank_diff == 2 && file_diff == 1) || (rank_diff == 1 && file_diff == 2)
2703 }
2704 chess::Piece::Bishop => self.is_diagonal_clear(board, piece_square, target_square),
2705 chess::Piece::Rook => self.is_rank_or_file_clear(board, piece_square, target_square),
2706 chess::Piece::Queen => {
2707 self.is_diagonal_clear(board, piece_square, target_square)
2708 || self.is_rank_or_file_clear(board, piece_square, target_square)
2709 }
2710 chess::Piece::King => {
2711 let source_rank = piece_square.get_rank().to_index() as i32;
2712 let source_file = piece_square.get_file().to_index() as i32;
2713 let target_rank = target_square.get_rank().to_index() as i32;
2714 let target_file = target_square.get_file().to_index() as i32;
2715
2716 let rank_diff = (target_rank - source_rank).abs();
2717 let file_diff = (target_file - source_file).abs();
2718
2719 rank_diff <= 1 && file_diff <= 1
2720 }
2721 }
2722 }
2723
2724 fn is_diagonal_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
2726 let from_rank = from.get_rank().to_index() as i32;
2727 let from_file = from.get_file().to_index() as i32;
2728 let to_rank = to.get_rank().to_index() as i32;
2729 let to_file = to.get_file().to_index() as i32;
2730
2731 let rank_diff = to_rank - from_rank;
2732 let file_diff = to_file - from_file;
2733
2734 if rank_diff.abs() != file_diff.abs() || rank_diff == 0 {
2736 return false;
2737 }
2738
2739 let rank_dir = if rank_diff > 0 { 1 } else { -1 };
2740 let file_dir = if file_diff > 0 { 1 } else { -1 };
2741
2742 let steps = rank_diff.abs();
2743
2744 for i in 1..steps {
2746 let check_rank = from_rank + (rank_dir * i);
2747 let check_file = from_file + (file_dir * i);
2748
2749 let check_square = chess::Square::make_square(
2750 chess::Rank::from_index(check_rank as usize),
2751 chess::File::from_index(check_file as usize),
2752 );
2753 if board.piece_on(check_square).is_some() {
2754 return false; }
2756 }
2757
2758 true
2759 }
2760
2761 fn is_rank_or_file_clear(&self, board: &Board, from: chess::Square, to: chess::Square) -> bool {
2763 let from_rank = from.get_rank().to_index() as i32;
2764 let from_file = from.get_file().to_index() as i32;
2765 let to_rank = to.get_rank().to_index() as i32;
2766 let to_file = to.get_file().to_index() as i32;
2767
2768 if from_rank != to_rank && from_file != to_file {
2770 return false;
2771 }
2772
2773 let (start, end, is_rank) = if from_rank == to_rank {
2774 let start = from_file.min(to_file);
2776 let end = from_file.max(to_file);
2777 (start, end, true)
2778 } else {
2779 let start = from_rank.min(to_rank);
2781 let end = from_rank.max(to_rank);
2782 (start, end, false)
2783 };
2784
2785 for i in (start + 1)..end {
2787 let check_square = if is_rank {
2788 chess::Square::make_square(
2789 chess::Rank::from_index(from_rank as usize),
2790 chess::File::from_index(i as usize),
2791 )
2792 } else {
2793 chess::Square::make_square(
2794 chess::Rank::from_index(i as usize),
2795 chess::File::from_index(from_file as usize),
2796 )
2797 };
2798
2799 if board.piece_on(check_square).is_some() {
2800 return false; }
2802 }
2803
2804 true
2805 }
2806
2807 pub fn get_piece_attackers(
2809 &self,
2810 board: &Board,
2811 square: chess::Square,
2812 color: chess::Color,
2813 ) -> Vec<chess::Piece> {
2814 let mut attackers = Vec::new();
2815
2816 let pieces = board.color_combined(color);
2818
2819 let pawns = board.pieces(chess::Piece::Pawn) & pieces;
2821 if pawns.popcnt() > 0 && self.can_pawn_attack(board, square, color) {
2822 attackers.push(chess::Piece::Pawn);
2823 }
2824
2825 let knights = board.pieces(chess::Piece::Knight) & pieces;
2827 if knights.popcnt() > 0 && self.can_piece_attack(board, square, chess::Piece::Knight, color)
2828 {
2829 attackers.push(chess::Piece::Knight);
2830 }
2831
2832 let bishops = board.pieces(chess::Piece::Bishop) & pieces;
2834 if bishops.popcnt() > 0 && self.can_piece_attack(board, square, chess::Piece::Bishop, color)
2835 {
2836 attackers.push(chess::Piece::Bishop);
2837 }
2838
2839 let rooks = board.pieces(chess::Piece::Rook) & pieces;
2841 if rooks.popcnt() > 0 && self.can_piece_attack(board, square, chess::Piece::Rook, color) {
2842 attackers.push(chess::Piece::Rook);
2843 }
2844
2845 let queens = board.pieces(chess::Piece::Queen) & pieces;
2847 if queens.popcnt() > 0 && self.can_piece_attack(board, square, chess::Piece::Queen, color) {
2848 attackers.push(chess::Piece::Queen);
2849 }
2850
2851 let kings = board.pieces(chess::Piece::King) & pieces;
2853 if kings.popcnt() > 0 && self.can_piece_attack(board, square, chess::Piece::King, color) {
2854 attackers.push(chess::Piece::King);
2855 }
2856
2857 attackers
2858 }
2859
2860 fn can_pawn_attack(&self, board: &Board, square: chess::Square, color: chess::Color) -> bool {
2862 let rank = square.get_rank().to_index() as i32;
2864 let file = square.get_file().to_index() as i32;
2865
2866 let (attack_rank, _direction) = if color == chess::Color::White {
2867 (rank - 1, 1)
2868 } else {
2869 (rank + 1, -1)
2870 };
2871
2872 if attack_rank < 0 || attack_rank > 7 {
2873 return false;
2874 }
2875
2876 for attack_file in [file - 1, file + 1] {
2878 if attack_file >= 0 && attack_file <= 7 {
2879 let pawn_square = chess::Square::make_square(
2880 chess::Rank::from_index(attack_rank as usize),
2881 chess::File::from_index(attack_file as usize),
2882 );
2883 if let Some(piece) = board.piece_on(pawn_square) {
2884 if piece == chess::Piece::Pawn && board.color_on(pawn_square) == Some(color) {
2885 return true;
2886 }
2887 }
2888 }
2889 }
2890
2891 false
2892 }
2893
2894 pub fn can_piece_attack(
2896 &self,
2897 board: &Board,
2898 target_square: chess::Square,
2899 piece_type: chess::Piece,
2900 color: chess::Color,
2901 ) -> bool {
2902 let pieces = board.pieces(piece_type) & board.color_combined(color);
2904
2905 for square in pieces {
2907 match piece_type {
2908 chess::Piece::Knight => {
2909 let knight_moves = [
2911 (-2, -1),
2912 (-2, 1),
2913 (-1, -2),
2914 (-1, 2),
2915 (1, -2),
2916 (1, 2),
2917 (2, -1),
2918 (2, 1),
2919 ];
2920
2921 let source_rank = square.get_rank().to_index() as i32;
2922 let source_file = square.get_file().to_index() as i32;
2923 let target_rank = target_square.get_rank().to_index() as i32;
2924 let target_file = target_square.get_file().to_index() as i32;
2925
2926 for (rank_offset, file_offset) in knight_moves {
2927 if source_rank + rank_offset == target_rank
2928 && source_file + file_offset == target_file
2929 {
2930 return true;
2931 }
2932 }
2933 }
2934 chess::Piece::Bishop => {
2935 let source_rank = square.get_rank().to_index() as i32;
2937 let source_file = square.get_file().to_index() as i32;
2938 let target_rank = target_square.get_rank().to_index() as i32;
2939 let target_file = target_square.get_file().to_index() as i32;
2940
2941 let rank_diff = (target_rank - source_rank).abs();
2942 let file_diff = (target_file - source_file).abs();
2943
2944 if rank_diff == file_diff && rank_diff > 0 {
2946 return true;
2948 }
2949 }
2950 chess::Piece::Rook => {
2951 let source_rank = square.get_rank().to_index();
2953 let source_file = square.get_file().to_index();
2954 let target_rank = target_square.get_rank().to_index();
2955 let target_file = target_square.get_file().to_index();
2956
2957 if source_rank == target_rank || source_file == target_file {
2959 return true; }
2961 }
2962 chess::Piece::Queen => {
2963 return self.can_piece_attack(board, target_square, chess::Piece::Rook, color)
2965 || self.can_piece_attack(
2966 board,
2967 target_square,
2968 chess::Piece::Bishop,
2969 color,
2970 );
2971 }
2972 chess::Piece::King => {
2973 let source_rank = square.get_rank().to_index() as i32;
2975 let source_file = square.get_file().to_index() as i32;
2976 let target_rank = target_square.get_rank().to_index() as i32;
2977 let target_file = target_square.get_file().to_index() as i32;
2978
2979 let rank_diff = (target_rank - source_rank).abs();
2980 let file_diff = (target_file - source_file).abs();
2981
2982 if rank_diff <= 1 && file_diff <= 1 && (rank_diff + file_diff) > 0 {
2983 return true;
2984 }
2985 }
2986 chess::Piece::Pawn => {
2987 continue;
2989 }
2990 }
2991 }
2992
2993 false
2994 }
2995
2996 fn is_killer_move_at_depth(&self, chess_move: &ChessMove, depth: u32) -> bool {
2998 let depth_idx = (depth as usize).min(self.killer_moves.len() - 1);
2999 self.killer_moves[depth_idx].contains(&Some(*chess_move))
3000 }
3001
3002 fn is_counter_move(&self, chess_move: &ChessMove) -> bool {
3004 if let Some(last_move) = self.last_move {
3005 let last_move_key = (last_move.get_source(), last_move.get_dest());
3006 if let Some(counter_move) = self.counter_moves.get(&last_move_key) {
3007 return *counter_move == *chess_move;
3008 }
3009 }
3010 false
3011 }
3012
3013 fn is_castling_move(&self, chess_move: &ChessMove, board: &Board) -> bool {
3015 if let Some(piece) = board.piece_on(chess_move.get_source()) {
3016 if piece == chess::Piece::King {
3017 let source_file = chess_move.get_source().get_file().to_index();
3018 let dest_file = chess_move.get_dest().get_file().to_index();
3019 return (source_file as i32 - dest_file as i32).abs() == 2;
3021 }
3022 }
3023 false
3024 }
3025
3026 fn gives_check(&self, chess_move: &ChessMove, board: &Board) -> bool {
3028 let new_board = board.make_move_new(*chess_move);
3029 new_board.checkers().popcnt() > 0
3030 }
3031
3032 fn mvv_lva_score(&self, chess_move: &ChessMove, board: &Board) -> i32 {
3034 let victim_value = if let Some(victim_piece) = board.piece_on(chess_move.get_dest()) {
3035 match victim_piece {
3036 chess::Piece::Pawn => 100,
3037 chess::Piece::Knight => 300,
3038 chess::Piece::Bishop => 300,
3039 chess::Piece::Rook => 500,
3040 chess::Piece::Queen => 900,
3041 chess::Piece::King => 10000, }
3043 } else {
3044 0
3045 };
3046
3047 let attacker_value = if let Some(attacker_piece) = board.piece_on(chess_move.get_source()) {
3048 match attacker_piece {
3049 chess::Piece::Pawn => 1,
3050 chess::Piece::Knight => 3,
3051 chess::Piece::Bishop => 3,
3052 chess::Piece::Rook => 5,
3053 chess::Piece::Queen => 9,
3054 chess::Piece::King => 1, }
3056 } else {
3057 1
3058 };
3059
3060 victim_value * 10 - attacker_value
3062 }
3063
3064 fn generate_captures(&self, board: &Board) -> Vec<ChessMove> {
3066 MoveGen::new_legal(board)
3067 .filter(|chess_move| {
3068 board.piece_on(chess_move.get_dest()).is_some()
3070 || chess_move.get_promotion().is_some()
3071 })
3072 .collect()
3073 }
3074
3075 #[allow(dead_code)]
3077 fn generate_checks(&self, board: &Board) -> Vec<ChessMove> {
3078 MoveGen::new_legal(board)
3079 .filter(|chess_move| {
3080 let new_board = board.make_move_new(*chess_move);
3082 new_board.checkers().popcnt() > 0
3083 })
3084 .collect()
3085 }
3086
3087 fn generate_captures_and_checks(&self, board: &Board) -> Vec<ChessMove> {
3089 MoveGen::new_legal(board)
3090 .filter(|chess_move| {
3091 let is_capture = board.piece_on(chess_move.get_dest()).is_some();
3093 let is_promotion = chess_move.get_promotion().is_some();
3094 let is_check = if !is_capture && !is_promotion {
3095 let new_board = board.make_move_new(*chess_move);
3097 new_board.checkers().popcnt() > 0
3098 } else {
3099 false
3100 };
3101
3102 is_capture || is_promotion || is_check
3103 })
3104 .collect()
3105 }
3106
3107 fn evaluate_position(&self, board: &Board) -> f32 {
3109 if board.status() != chess::BoardStatus::Ongoing {
3110 return self.evaluate_terminal_position(board);
3111 }
3112
3113 self.evaluate_position_traditional(board)
3121 }
3122
3123 fn evaluate_position_hybrid(&self, board: &Board) -> f32 {
3125 let (nnue_eval, nnue_confidence) = self.get_nnue_evaluation(board);
3129
3130 let (pattern_eval, pattern_confidence) = self.get_pattern_evaluation(board);
3132
3133 let combined_confidence = (nnue_confidence * 0.6) + (pattern_confidence * 0.4);
3135
3136 if combined_confidence >= self.config.pattern_confidence_threshold {
3138 let pattern_weight = self.config.pattern_weight * combined_confidence;
3140 let nnue_weight = 1.0 - pattern_weight;
3141
3142 (pattern_eval * pattern_weight) + (nnue_eval * nnue_weight)
3143 } else {
3144 let nnue_weight = 0.8;
3146 let pattern_weight = 0.2;
3147
3148 (nnue_eval * nnue_weight) + (pattern_eval * pattern_weight)
3149 }
3150 }
3151
3152 fn evaluate_position_traditional(&self, board: &Board) -> f32 {
3154 let score_cp = self.material_balance(board);
3156
3157 let score = (score_cp / 100.0).clamp(-4.0, 4.0);
3159
3160 score
3162 }
3163
3164 fn evaluate_terminal_position(&self, board: &Board) -> f32 {
3166 match board.status() {
3167 chess::BoardStatus::Checkmate => {
3168 if board.side_to_move() == Color::White {
3169 -10.0 } else {
3171 10.0 }
3173 }
3174 chess::BoardStatus::Stalemate => 0.0,
3175 _ => 0.0,
3176 }
3177 }
3178
3179 fn material_balance(&self, board: &Board) -> f32 {
3181 let mut balance = 0.0;
3183
3184 balance += (board.pieces(chess::Piece::Pawn) & board.color_combined(Color::White)).popcnt()
3186 as f32
3187 * 100.0;
3188 balance -= (board.pieces(chess::Piece::Pawn) & board.color_combined(Color::Black)).popcnt()
3189 as f32
3190 * 100.0;
3191
3192 balance += (board.pieces(chess::Piece::Knight) & board.color_combined(Color::White))
3193 .popcnt() as f32
3194 * 320.0;
3195 balance -= (board.pieces(chess::Piece::Knight) & board.color_combined(Color::Black))
3196 .popcnt() as f32
3197 * 320.0;
3198
3199 balance += (board.pieces(chess::Piece::Bishop) & board.color_combined(Color::White))
3200 .popcnt() as f32
3201 * 330.0;
3202 balance -= (board.pieces(chess::Piece::Bishop) & board.color_combined(Color::Black))
3203 .popcnt() as f32
3204 * 330.0;
3205
3206 balance += (board.pieces(chess::Piece::Rook) & board.color_combined(Color::White)).popcnt()
3207 as f32
3208 * 500.0;
3209 balance -= (board.pieces(chess::Piece::Rook) & board.color_combined(Color::Black)).popcnt()
3210 as f32
3211 * 500.0;
3212
3213 balance += (board.pieces(chess::Piece::Queen) & board.color_combined(Color::White)).popcnt()
3214 as f32
3215 * 900.0;
3216 balance -= (board.pieces(chess::Piece::Queen) & board.color_combined(Color::Black)).popcnt()
3217 as f32
3218 * 900.0;
3219
3220 balance }
3222
3223 fn piece_square_evaluation(&self, board: &Board) -> f32 {
3225 let mut score = 0.0;
3226 let game_phase = self.detect_game_phase(board);
3227
3228 let pawn_opening = [
3230 0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, 50, 50, 50, 50, 50, 10, 10, 20, 30, 30, 20, 10, 10,
3231 5, 5, 10, 27, 27, 10, 5, 5, 0, 0, 0, 25, 25, 0, 0, 0, 5, -5, -10, 0, 0, -10, -5, 5, 5,
3232 10, 10, -25, -25, 10, 10, 5, 0, 0, 0, 0, 0, 0, 0, 0,
3233 ];
3234
3235 let pawn_endgame = [
3236 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80, 50, 50, 50, 50, 50, 50, 50, 50,
3237 30, 30, 30, 30, 30, 30, 30, 30, 20, 20, 20, 20, 20, 20, 20, 20, 10, 10, 10, 10, 10, 10,
3238 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0,
3239 ];
3240
3241 let knight_opening = [
3243 -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 0, 0, 0, -20, -40, -30, 0, 10, 15,
3244 15, 10, 0, -30, -30, 5, 15, 20, 20, 15, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, -30, 5,
3245 10, 15, 15, 10, 5, -30, -40, -20, 0, 5, 5, 0, -20, -40, -50, -40, -30, -30, -30, -30,
3246 -40, -50,
3247 ];
3248
3249 let knight_endgame = [
3250 -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 5, 5, 0, -20, -40, -30, 0, 10, 15,
3251 15, 10, 0, -30, -30, 5, 15, 20, 20, 15, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, -30, 5,
3252 10, 15, 15, 10, 5, -30, -40, -20, 0, 5, 5, 0, -20, -40, -50, -40, -30, -30, -30, -30,
3253 -40, -50,
3254 ];
3255
3256 let bishop_opening = [
3258 -20, -10, -10, -10, -10, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 10, 10,
3259 5, 0, -10, -10, 5, 5, 10, 10, 5, 5, -10, -10, 0, 10, 10, 10, 10, 0, -10, -10, 10, 10,
3260 10, 10, 10, 10, -10, -10, 5, 0, 0, 0, 0, 5, -10, -20, -10, -10, -10, -10, -10, -10,
3261 -20,
3262 ];
3263
3264 let bishop_endgame = [
3265 -20, -10, -10, -10, -10, -10, -10, -20, -10, 5, 0, 0, 0, 0, 5, -10, -10, 0, 10, 15, 15,
3266 10, 0, -10, -10, 0, 15, 20, 20, 15, 0, -10, -10, 0, 15, 20, 20, 15, 0, -10, -10, 0, 10,
3267 15, 15, 10, 0, -10, -10, 5, 0, 0, 0, 0, 5, -10, -20, -10, -10, -10, -10, -10, -10, -20,
3268 ];
3269
3270 let rook_opening = [
3272 0, 0, 0, 0, 0, 0, 0, 0, 5, 10, 10, 10, 10, 10, 10, 5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0,
3273 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0,
3274 0, 0, -5, 0, 0, 0, 5, 5, 0, 0, 0,
3275 ];
3276
3277 let rook_endgame = [
3278 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3279 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3280 0, 0, 0, 0, 0, 0, 0, 0, 0,
3281 ];
3282
3283 let queen_opening = [
3285 -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 5, 5, 5,
3286 0, -10, -5, 0, 5, 5, 5, 5, 0, -5, 0, 0, 5, 5, 5, 5, 0, -5, -10, 5, 5, 5, 5, 5, 0, -10,
3287 -10, 0, 5, 0, 0, 0, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20,
3288 ];
3289
3290 let queen_endgame = [
3291 -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 5, 5, 5, 5, 0, -10, -10, 5, 10, 10, 10,
3292 10, 5, -10, -5, 0, 10, 10, 10, 10, 0, -5, -5, 0, 10, 10, 10, 10, 0, -5, -10, 5, 10, 10,
3293 10, 10, 5, -10, -10, 0, 5, 5, 5, 5, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20,
3294 ];
3295
3296 let king_opening = [
3298 -30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, -30,
3299 -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, -20, -30,
3300 -30, -40, -40, -30, -30, -20, -10, -20, -20, -20, -20, -20, -20, -10, 20, 20, 0, 0, 0,
3301 0, 20, 20, 20, 30, 10, 0, 0, 10, 30, 20,
3302 ];
3303
3304 let king_endgame = [
3305 -50, -40, -30, -20, -20, -30, -40, -50, -30, -20, -10, 0, 0, -10, -20, -30, -30, -10,
3306 20, 30, 30, 20, -10, -30, -30, -10, 30, 40, 40, 30, -10, -30, -30, -10, 30, 40, 40, 30,
3307 -10, -30, -30, -10, 20, 30, 30, 20, -10, -30, -30, -30, 0, 0, 0, 0, -30, -30, -50, -30,
3308 -30, -30, -30, -30, -30, -50,
3309 ];
3310
3311 let phase_factor = match game_phase {
3313 GamePhase::Opening => 1.0,
3314 GamePhase::Middlegame => 0.5,
3315 GamePhase::Endgame => 0.0,
3316 };
3317
3318 for color in [Color::White, Color::Black] {
3320 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3321
3322 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3324 for square in pawns {
3325 let idx = if color == Color::White {
3326 square.to_index()
3327 } else {
3328 square.to_index() ^ 56
3329 };
3330 let opening_value = pawn_opening[idx] as f32;
3331 let endgame_value = pawn_endgame[idx] as f32;
3332 let interpolated_value =
3333 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
3334 score += interpolated_value * multiplier * 0.01; }
3336
3337 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
3339 for square in knights {
3340 let idx = if color == Color::White {
3341 square.to_index()
3342 } else {
3343 square.to_index() ^ 56
3344 };
3345 let opening_value = knight_opening[idx] as f32;
3346 let endgame_value = knight_endgame[idx] as f32;
3347 let interpolated_value =
3348 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
3349 score += interpolated_value * multiplier * 0.01;
3350 }
3351
3352 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3354 for square in bishops {
3355 let idx = if color == Color::White {
3356 square.to_index()
3357 } else {
3358 square.to_index() ^ 56
3359 };
3360 let opening_value = bishop_opening[idx] as f32;
3361 let endgame_value = bishop_endgame[idx] as f32;
3362 let interpolated_value =
3363 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
3364 score += interpolated_value * multiplier * 0.01;
3365 }
3366
3367 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3369 for square in rooks {
3370 let idx = if color == Color::White {
3371 square.to_index()
3372 } else {
3373 square.to_index() ^ 56
3374 };
3375 let opening_value = rook_opening[idx] as f32;
3376 let endgame_value = rook_endgame[idx] as f32;
3377 let interpolated_value =
3378 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
3379 score += interpolated_value * multiplier * 0.01;
3380 }
3381
3382 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
3384 for square in queens {
3385 let idx = if color == Color::White {
3386 square.to_index()
3387 } else {
3388 square.to_index() ^ 56
3389 };
3390 let opening_value = queen_opening[idx] as f32;
3391 let endgame_value = queen_endgame[idx] as f32;
3392 let interpolated_value =
3393 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
3394 score += interpolated_value * multiplier * 0.01;
3395 }
3396
3397 let kings = board.pieces(chess::Piece::King) & board.color_combined(color);
3399 for square in kings {
3400 let idx = if color == Color::White {
3401 square.to_index()
3402 } else {
3403 square.to_index() ^ 56
3404 };
3405 let opening_value = king_opening[idx] as f32;
3406 let endgame_value = king_endgame[idx] as f32;
3407 let interpolated_value =
3408 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
3409 score += interpolated_value * multiplier * 0.01;
3410 }
3411 }
3412
3413 score
3414 }
3415
3416 fn detect_game_phase(&self, board: &Board) -> GamePhase {
3418 let mut total_material = 0;
3419
3420 for color in [Color::White, Color::Black] {
3422 total_material +=
3423 (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt() * 9;
3424 total_material +=
3425 (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt() * 5;
3426 total_material +=
3427 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt() * 3;
3428 total_material +=
3429 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt() * 3;
3430 }
3431
3432 if total_material >= 60 {
3434 GamePhase::Opening
3435 } else if total_material >= 20 {
3436 GamePhase::Middlegame
3437 } else {
3438 GamePhase::Endgame
3439 }
3440 }
3441
3442 fn mobility_evaluation(&self, board: &Board) -> f32 {
3444 let mut score = 0.0;
3445
3446 for color in [Color::White, Color::Black] {
3447 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3448 let mobility_score = self.calculate_piece_mobility(board, color);
3449 score += mobility_score * multiplier;
3450 }
3451
3452 score
3453 }
3454
3455 fn calculate_piece_mobility(&self, board: &Board, color: Color) -> f32 {
3457 let mut mobility = 0.0;
3458
3459 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
3461 for knight_square in knights {
3462 let knight_moves = self.count_knight_moves(board, knight_square, color);
3463 mobility += knight_moves as f32 * 4.0; }
3465
3466 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3468 for bishop_square in bishops {
3469 let bishop_moves = self.count_bishop_moves(board, bishop_square, color);
3470 mobility += bishop_moves as f32 * 3.0; }
3472
3473 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3475 for rook_square in rooks {
3476 let rook_moves = self.count_rook_moves(board, rook_square, color);
3477 mobility += rook_moves as f32 * 2.0; }
3479
3480 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
3482 for queen_square in queens {
3483 let queen_moves = self.count_queen_moves(board, queen_square, color);
3484 mobility += queen_moves as f32 * 1.0; }
3486
3487 let pawn_mobility = self.calculate_pawn_mobility(board, color);
3489 mobility += pawn_mobility * 5.0; mobility
3492 }
3493
3494 fn count_knight_moves(&self, board: &Board, square: Square, color: Color) -> usize {
3496 let mut count = 0;
3497 let knight_offsets = [
3498 (-2, -1),
3499 (-2, 1),
3500 (-1, -2),
3501 (-1, 2),
3502 (1, -2),
3503 (1, 2),
3504 (2, -1),
3505 (2, 1),
3506 ];
3507
3508 let file = square.get_file().to_index() as i8;
3509 let rank = square.get_rank().to_index() as i8;
3510
3511 for (df, dr) in knight_offsets {
3512 let new_file = file + df;
3513 let new_rank = rank + dr;
3514
3515 if (0..8).contains(&new_file) && (0..8).contains(&new_rank) {
3516 let dest_square = Square::make_square(
3517 chess::Rank::from_index(new_rank as usize),
3518 chess::File::from_index(new_file as usize),
3519 );
3520 if let Some(_piece_on_dest) = board.piece_on(dest_square) {
3522 if board.color_on(dest_square) != Some(color) {
3523 count += 1; }
3525 } else {
3526 count += 1; }
3528 }
3529 }
3530
3531 count
3532 }
3533
3534 fn count_bishop_moves(&self, board: &Board, square: Square, color: Color) -> usize {
3536 let mut count = 0;
3537 let directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)];
3538
3539 for (df, dr) in directions {
3540 count += self.count_sliding_moves(board, square, color, df, dr);
3541 }
3542
3543 count
3544 }
3545
3546 fn count_rook_moves(&self, board: &Board, square: Square, color: Color) -> usize {
3548 let mut count = 0;
3549 let directions = [(1, 0), (-1, 0), (0, 1), (0, -1)];
3550
3551 for (df, dr) in directions {
3552 count += self.count_sliding_moves(board, square, color, df, dr);
3553 }
3554
3555 count
3556 }
3557
3558 fn count_queen_moves(&self, board: &Board, square: Square, color: Color) -> usize {
3560 let mut count = 0;
3561 let directions = [
3562 (1, 0),
3563 (-1, 0),
3564 (0, 1),
3565 (0, -1), (1, 1),
3567 (1, -1),
3568 (-1, 1),
3569 (-1, -1), ];
3571
3572 for (df, dr) in directions {
3573 count += self.count_sliding_moves(board, square, color, df, dr);
3574 }
3575
3576 count
3577 }
3578
3579 fn count_sliding_moves(
3581 &self,
3582 board: &Board,
3583 square: Square,
3584 color: Color,
3585 df: i8,
3586 dr: i8,
3587 ) -> usize {
3588 let mut count = 0;
3589 let mut file = square.get_file().to_index() as i8;
3590 let mut rank = square.get_rank().to_index() as i8;
3591
3592 loop {
3593 file += df;
3594 rank += dr;
3595
3596 if !(0..8).contains(&file) || !(0..8).contains(&rank) {
3597 break;
3598 }
3599
3600 let dest_square = Square::make_square(
3601 chess::Rank::from_index(rank as usize),
3602 chess::File::from_index(file as usize),
3603 );
3604 if let Some(_piece_on_dest) = board.piece_on(dest_square) {
3605 if board.color_on(dest_square) != Some(color) {
3606 count += 1; }
3608 break; } else {
3610 count += 1; }
3612 }
3613
3614 count
3615 }
3616
3617 fn calculate_pawn_mobility(&self, board: &Board, color: Color) -> f32 {
3619 let mut mobility = 0.0;
3620 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3621
3622 let direction = if color == Color::White { 1 } else { -1 };
3623
3624 for pawn_square in pawns {
3625 let file = pawn_square.get_file().to_index() as i8;
3626 let rank = pawn_square.get_rank().to_index() as i8;
3627
3628 let advance_rank = rank + direction;
3630 if (0..8).contains(&advance_rank) {
3631 let advance_square = Square::make_square(
3632 chess::Rank::from_index(advance_rank as usize),
3633 pawn_square.get_file(),
3634 );
3635 if board.piece_on(advance_square).is_none() {
3636 mobility += 1.0; let starting_rank = if color == Color::White { 1 } else { 6 };
3640 if rank == starting_rank {
3641 let double_advance_rank = advance_rank + direction;
3642 let double_advance_square = Square::make_square(
3643 chess::Rank::from_index(double_advance_rank as usize),
3644 pawn_square.get_file(),
3645 );
3646 if board.piece_on(double_advance_square).is_none() {
3647 mobility += 0.5; }
3649 }
3650 }
3651 }
3652
3653 for capture_file in [file - 1, file + 1] {
3655 if (0..8).contains(&capture_file) && (0..8).contains(&advance_rank) {
3656 let capture_square = Square::make_square(
3657 chess::Rank::from_index(advance_rank as usize),
3658 chess::File::from_index(capture_file as usize),
3659 );
3660 if let Some(_piece) = board.piece_on(capture_square) {
3661 if board.color_on(capture_square) != Some(color) {
3662 mobility += 2.0; }
3664 }
3665 }
3666 }
3667 }
3668
3669 mobility
3670 }
3671
3672 fn tactical_bonuses(&self, board: &Board) -> f32 {
3674 let mut bonus = 0.0;
3676
3677 let captures = MoveGen::new_legal(board)
3679 .filter(|m| board.piece_on(m.get_dest()).is_some())
3680 .count();
3681 let capture_bonus = captures as f32 * 10.0; if board.side_to_move() == Color::White {
3685 bonus += capture_bonus;
3686 } else {
3687 bonus -= capture_bonus;
3688 }
3689
3690 bonus
3691 }
3692
3693 fn center_control_evaluation(&self, board: &Board) -> f32 {
3695 let mut score = 0.0;
3696 let center_squares = [
3697 Square::make_square(chess::Rank::Fourth, chess::File::D),
3698 Square::make_square(chess::Rank::Fourth, chess::File::E),
3699 Square::make_square(chess::Rank::Fifth, chess::File::D),
3700 Square::make_square(chess::Rank::Fifth, chess::File::E),
3701 ];
3702
3703 let extended_center = [
3704 Square::make_square(chess::Rank::Third, chess::File::C),
3705 Square::make_square(chess::Rank::Third, chess::File::D),
3706 Square::make_square(chess::Rank::Third, chess::File::E),
3707 Square::make_square(chess::Rank::Third, chess::File::F),
3708 Square::make_square(chess::Rank::Fourth, chess::File::C),
3709 Square::make_square(chess::Rank::Fourth, chess::File::F),
3710 Square::make_square(chess::Rank::Fifth, chess::File::C),
3711 Square::make_square(chess::Rank::Fifth, chess::File::F),
3712 Square::make_square(chess::Rank::Sixth, chess::File::C),
3713 Square::make_square(chess::Rank::Sixth, chess::File::D),
3714 Square::make_square(chess::Rank::Sixth, chess::File::E),
3715 Square::make_square(chess::Rank::Sixth, chess::File::F),
3716 ];
3717
3718 for &square in ¢er_squares {
3720 if let Some(piece) = board.piece_on(square) {
3721 if piece == chess::Piece::Pawn {
3722 if let Some(color) = board.color_on(square) {
3723 let bonus = if color == Color::White { 30.0 } else { -30.0 };
3724 score += bonus;
3725 }
3726 }
3727 }
3728 }
3729
3730 for &square in &extended_center {
3732 if let Some(_piece) = board.piece_on(square) {
3733 if let Some(color) = board.color_on(square) {
3734 let bonus = if color == Color::White { 5.0 } else { -5.0 };
3735 score += bonus;
3736 }
3737 }
3738 }
3739
3740 score
3741 }
3742
3743 fn king_safety(&self, board: &Board) -> f32 {
3745 let mut safety = 0.0;
3746 let game_phase = self.detect_game_phase(board);
3747
3748 for color in [Color::White, Color::Black] {
3749 let mut king_safety = 0.0;
3750 let king_square = board.king_square(color);
3751 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3752
3753 king_safety += self.evaluate_castling_safety(board, color, king_square, game_phase);
3755
3756 king_safety += self.evaluate_pawn_shield(board, color, king_square, game_phase);
3758
3759 king_safety += self.evaluate_king_attackers(board, color, king_square);
3761
3762 king_safety += self.evaluate_open_lines_near_king(board, color, king_square);
3764
3765 if game_phase == GamePhase::Endgame {
3767 king_safety += self.evaluate_king_endgame_activity(board, color, king_square);
3768 }
3769
3770 king_safety += self.evaluate_king_zone_control(board, color, king_square);
3772
3773 if board.checkers().popcnt() > 0 && board.side_to_move() == color {
3775 let check_severity = self.evaluate_check_severity(board, color);
3776 king_safety -= check_severity;
3777 }
3778
3779 safety += king_safety * multiplier;
3780 }
3781
3782 safety
3783 }
3784
3785 fn evaluate_castling_safety(
3787 &self,
3788 board: &Board,
3789 color: Color,
3790 king_square: Square,
3791 game_phase: GamePhase,
3792 ) -> f32 {
3793 let mut score = 0.0;
3794
3795 let starting_square = if color == Color::White {
3796 Square::E1
3797 } else {
3798 Square::E8
3799 };
3800 let kingside_castle = if color == Color::White {
3801 Square::G1
3802 } else {
3803 Square::G8
3804 };
3805 let queenside_castle = if color == Color::White {
3806 Square::C1
3807 } else {
3808 Square::C8
3809 };
3810
3811 match game_phase {
3812 GamePhase::Opening | GamePhase::Middlegame => {
3813 if king_square == kingside_castle {
3814 score += 50.0; } else if king_square == queenside_castle {
3816 score += 35.0; } else if king_square == starting_square {
3818 let castle_rights = board.castle_rights(color);
3820 if castle_rights.has_kingside() {
3821 score += 25.0;
3822 }
3823 if castle_rights.has_queenside() {
3824 score += 15.0;
3825 }
3826 } else {
3827 score -= 80.0;
3829 }
3830 }
3831 GamePhase::Endgame => {
3832 let rank = king_square.get_rank().to_index() as i8;
3834 let file = king_square.get_file().to_index() as i8;
3835 let center_distance = (rank as f32 - 3.5).abs() + (file as f32 - 3.5).abs();
3836 score += (7.0 - center_distance) * 5.0; }
3838 }
3839
3840 score
3841 }
3842
3843 fn evaluate_pawn_shield(
3845 &self,
3846 board: &Board,
3847 color: Color,
3848 king_square: Square,
3849 game_phase: GamePhase,
3850 ) -> f32 {
3851 if game_phase == GamePhase::Endgame {
3852 return 0.0; }
3854
3855 let mut shield_score = 0.0;
3856 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3857 let king_file = king_square.get_file().to_index() as i8;
3858 let king_rank = king_square.get_rank().to_index() as i8;
3859
3860 let shield_files = [king_file - 1, king_file, king_file + 1];
3862 let forward_direction = if color == Color::White { 1 } else { -1 };
3863
3864 for &file in &shield_files {
3865 if (0..8).contains(&file) {
3866 let mut found_pawn = false;
3867 let file_mask = self.get_file_mask(chess::File::from_index(file as usize));
3868 let file_pawns = pawns & file_mask;
3869
3870 for pawn_square in file_pawns {
3871 let pawn_rank = pawn_square.get_rank().to_index() as i8;
3872 let rank_distance = (pawn_rank - king_rank) * forward_direction;
3873
3874 if rank_distance > 0 && rank_distance <= 3 {
3875 found_pawn = true;
3876 let protection_value = match rank_distance {
3878 1 => 25.0, 2 => 15.0, 3 => 8.0, _ => 0.0,
3882 };
3883 shield_score += protection_value;
3884 break;
3885 }
3886 }
3887
3888 if !found_pawn {
3890 shield_score -= 20.0;
3891 }
3892 }
3893 }
3894
3895 let is_kingside = king_file >= 6;
3897 let is_queenside = king_file <= 2;
3898
3899 if is_kingside {
3900 shield_score += self.evaluate_kingside_pawn_structure(board, color);
3901 } else if is_queenside {
3902 shield_score += self.evaluate_queenside_pawn_structure(board, color);
3903 }
3904
3905 shield_score
3906 }
3907
3908 fn evaluate_kingside_pawn_structure(&self, board: &Board, color: Color) -> f32 {
3910 let mut score = 0.0;
3911 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3912 let base_rank = if color == Color::White { 1 } else { 6 };
3913
3914 for (file_idx, ideal_rank) in [(5, base_rank), (6, base_rank), (7, base_rank)] {
3916 let file_mask = self.get_file_mask(chess::File::from_index(file_idx));
3917 let file_pawns = pawns & file_mask;
3918
3919 let mut found_intact = false;
3920 for pawn_square in file_pawns {
3921 if pawn_square.get_rank().to_index() == ideal_rank {
3922 found_intact = true;
3923 score += 10.0; break;
3925 }
3926 }
3927
3928 if !found_intact {
3929 score -= 15.0; }
3931 }
3932
3933 score
3934 }
3935
3936 fn evaluate_queenside_pawn_structure(&self, board: &Board, color: Color) -> f32 {
3938 let mut score = 0.0;
3939 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3940 let base_rank = if color == Color::White { 1 } else { 6 };
3941
3942 for (file_idx, ideal_rank) in [(0, base_rank), (1, base_rank), (2, base_rank)] {
3944 let file_mask = self.get_file_mask(chess::File::from_index(file_idx));
3945 let file_pawns = pawns & file_mask;
3946
3947 let mut found_intact = false;
3948 for pawn_square in file_pawns {
3949 if pawn_square.get_rank().to_index() == ideal_rank {
3950 found_intact = true;
3951 score += 8.0; break;
3953 }
3954 }
3955
3956 if !found_intact {
3957 score -= 12.0; }
3959 }
3960
3961 score
3962 }
3963
3964 fn evaluate_king_attackers(&self, board: &Board, color: Color, king_square: Square) -> f32 {
3966 let mut attack_score = 0.0;
3967 let enemy_color = !color;
3968
3969 let enemy_queens = board.pieces(chess::Piece::Queen) & board.color_combined(enemy_color);
3971 let enemy_rooks = board.pieces(chess::Piece::Rook) & board.color_combined(enemy_color);
3972 let enemy_bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(enemy_color);
3973 let enemy_knights = board.pieces(chess::Piece::Knight) & board.color_combined(enemy_color);
3974
3975 for queen_square in enemy_queens {
3977 if self.can_attack_square(board, queen_square, king_square, chess::Piece::Queen) {
3978 attack_score -= 50.0;
3979 }
3980 }
3981
3982 for rook_square in enemy_rooks {
3984 if self.can_attack_square(board, rook_square, king_square, chess::Piece::Rook) {
3985 attack_score -= 30.0;
3986 }
3987 }
3988
3989 for bishop_square in enemy_bishops {
3991 if self.can_attack_square(board, bishop_square, king_square, chess::Piece::Bishop) {
3992 attack_score -= 25.0;
3993 }
3994 }
3995
3996 for knight_square in enemy_knights {
3998 if self.can_attack_square(board, knight_square, king_square, chess::Piece::Knight) {
3999 attack_score -= 20.0;
4000 }
4001 }
4002
4003 attack_score
4004 }
4005
4006 fn can_attack_square(
4008 &self,
4009 board: &Board,
4010 piece_square: Square,
4011 target_square: Square,
4012 piece_type: chess::Piece,
4013 ) -> bool {
4014 match piece_type {
4015 chess::Piece::Queen | chess::Piece::Rook | chess::Piece::Bishop => {
4016 self.has_clear_line_of_attack(board, piece_square, target_square, piece_type)
4018 }
4019 chess::Piece::Knight => {
4020 let file_diff = (piece_square.get_file().to_index() as i8
4022 - target_square.get_file().to_index() as i8)
4023 .abs();
4024 let rank_diff = (piece_square.get_rank().to_index() as i8
4025 - target_square.get_rank().to_index() as i8)
4026 .abs();
4027 (file_diff == 2 && rank_diff == 1) || (file_diff == 1 && rank_diff == 2)
4028 }
4029 _ => false,
4030 }
4031 }
4032
4033 fn has_clear_line_of_attack(
4035 &self,
4036 board: &Board,
4037 from: Square,
4038 to: Square,
4039 piece_type: chess::Piece,
4040 ) -> bool {
4041 let from_file = from.get_file().to_index() as i8;
4042 let from_rank = from.get_rank().to_index() as i8;
4043 let to_file = to.get_file().to_index() as i8;
4044 let to_rank = to.get_rank().to_index() as i8;
4045
4046 let file_diff = to_file - from_file;
4047 let rank_diff = to_rank - from_rank;
4048
4049 let is_valid_attack = match piece_type {
4051 chess::Piece::Rook | chess::Piece::Queen => {
4052 file_diff == 0 || rank_diff == 0 || file_diff.abs() == rank_diff.abs()
4053 }
4054 chess::Piece::Bishop => file_diff.abs() == rank_diff.abs(),
4055 _ => false,
4056 };
4057
4058 if !is_valid_attack {
4059 return false;
4060 }
4061
4062 let file_step = if file_diff == 0 {
4064 0
4065 } else {
4066 file_diff.signum()
4067 };
4068 let rank_step = if rank_diff == 0 {
4069 0
4070 } else {
4071 rank_diff.signum()
4072 };
4073
4074 let mut current_file = from_file + file_step;
4075 let mut current_rank = from_rank + rank_step;
4076
4077 while current_file != to_file || current_rank != to_rank {
4078 let square = Square::make_square(
4079 chess::Rank::from_index(current_rank as usize),
4080 chess::File::from_index(current_file as usize),
4081 );
4082 if board.piece_on(square).is_some() {
4083 return false; }
4085 current_file += file_step;
4086 current_rank += rank_step;
4087 }
4088
4089 true
4090 }
4091
4092 fn evaluate_open_lines_near_king(
4094 &self,
4095 board: &Board,
4096 color: Color,
4097 king_square: Square,
4098 ) -> f32 {
4099 let mut line_score = 0.0;
4100 let king_file = king_square.get_file();
4101 let _king_rank = king_square.get_rank();
4102
4103 for file_offset in -1..=1i8 {
4105 let file_index = (king_file.to_index() as i8 + file_offset).clamp(0, 7) as usize;
4106 let file = chess::File::from_index(file_index);
4107 if self.is_open_file(board, file) {
4108 line_score -= 20.0; } else if self.is_semi_open_file(board, file, color) {
4110 line_score -= 10.0; }
4112 }
4113
4114 line_score += self.evaluate_diagonal_safety(board, color, king_square);
4116
4117 line_score
4118 }
4119
4120 fn is_open_file(&self, board: &Board, file: chess::File) -> bool {
4122 let file_mask = self.get_file_mask(file);
4123 let all_pawns = board.pieces(chess::Piece::Pawn);
4124 (all_pawns & file_mask).popcnt() == 0
4125 }
4126
4127 fn is_semi_open_file(&self, board: &Board, file: chess::File, color: Color) -> bool {
4129 let file_mask = self.get_file_mask(file);
4130 let own_pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4131 (own_pawns & file_mask).popcnt() == 0
4132 }
4133
4134 fn evaluate_diagonal_safety(&self, board: &Board, color: Color, king_square: Square) -> f32 {
4136 let mut score = 0.0;
4137 let enemy_color = !color;
4138 let enemy_bishops_queens = (board.pieces(chess::Piece::Bishop)
4139 | board.pieces(chess::Piece::Queen))
4140 & board.color_combined(enemy_color);
4141
4142 let directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)];
4144
4145 for (file_dir, rank_dir) in directions {
4146 if self.has_diagonal_threat(
4147 board,
4148 king_square,
4149 file_dir,
4150 rank_dir,
4151 enemy_bishops_queens,
4152 ) {
4153 score -= 15.0; }
4155 }
4156
4157 score
4158 }
4159
4160 fn has_diagonal_threat(
4162 &self,
4163 board: &Board,
4164 king_square: Square,
4165 file_dir: i8,
4166 rank_dir: i8,
4167 enemy_pieces: chess::BitBoard,
4168 ) -> bool {
4169 let mut file = king_square.get_file().to_index() as i8 + file_dir;
4170 let mut rank = king_square.get_rank().to_index() as i8 + rank_dir;
4171
4172 while (0..8).contains(&file) && (0..8).contains(&rank) {
4173 let square = Square::make_square(
4174 chess::Rank::from_index(rank as usize),
4175 chess::File::from_index(file as usize),
4176 );
4177 if let Some(_piece) = board.piece_on(square) {
4178 return (enemy_pieces & chess::BitBoard::from_square(square)).popcnt() > 0;
4180 }
4181 file += file_dir;
4182 rank += rank_dir;
4183 }
4184
4185 false
4186 }
4187
4188 fn evaluate_king_endgame_activity(
4190 &self,
4191 board: &Board,
4192 color: Color,
4193 king_square: Square,
4194 ) -> f32 {
4195 let mut activity_score = 0.0;
4196
4197 let file = king_square.get_file().to_index() as f32;
4199 let rank = king_square.get_rank().to_index() as f32;
4200 let center_distance = ((file - 3.5).abs() + (rank - 3.5).abs()) / 2.0;
4201 activity_score += (3.5 - center_distance) * 10.0;
4202
4203 let enemy_pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(!color);
4205 for enemy_pawn in enemy_pawns {
4206 let distance = ((king_square.get_file().to_index() as i8
4207 - enemy_pawn.get_file().to_index() as i8)
4208 .abs()
4209 + (king_square.get_rank().to_index() as i8
4210 - enemy_pawn.get_rank().to_index() as i8)
4211 .abs()) as f32;
4212 if distance <= 3.0 {
4213 activity_score += 5.0; }
4215 }
4216
4217 activity_score
4218 }
4219
4220 fn evaluate_king_zone_control(&self, board: &Board, color: Color, king_square: Square) -> f32 {
4222 let mut control_score = 0.0;
4223 let king_file = king_square.get_file().to_index() as i8;
4224 let king_rank = king_square.get_rank().to_index() as i8;
4225
4226 for file_offset in -1..=1 {
4228 for rank_offset in -1..=1 {
4229 if file_offset == 0 && rank_offset == 0 {
4230 continue; }
4232
4233 let check_file = king_file + file_offset;
4234 let check_rank = king_rank + rank_offset;
4235
4236 if (0..8).contains(&check_file) && (0..8).contains(&check_rank) {
4237 let square = Square::make_square(
4238 chess::Rank::from_index(check_rank as usize),
4239 chess::File::from_index(check_file as usize),
4240 );
4241 if let Some(_piece) = board.piece_on(square) {
4242 if board.color_on(square) == Some(color) {
4243 control_score += 3.0; } else {
4245 control_score -= 5.0; }
4247 }
4248 }
4249 }
4250 }
4251
4252 control_score
4253 }
4254
4255 fn evaluate_check_severity(&self, board: &Board, _color: Color) -> f32 {
4257 let checkers = board.checkers();
4258 let check_count = checkers.popcnt();
4259
4260 let base_penalty = match check_count {
4261 0 => 0.0,
4262 1 => 50.0, 2 => 150.0, _ => 200.0, };
4266
4267 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
4269 let king_moves = legal_moves
4270 .iter()
4271 .filter(|mv| board.piece_on(mv.get_source()) == Some(chess::Piece::King))
4272 .count();
4273
4274 let escape_penalty = match king_moves {
4275 0 => 100.0, 1 => 30.0, 2 => 15.0, _ => 0.0, };
4280
4281 base_penalty + escape_penalty
4282 }
4283
4284 fn determine_game_phase(&self, board: &Board) -> GamePhase {
4286 let mut material_count = 0;
4288
4289 for piece in [
4290 chess::Piece::Queen,
4291 chess::Piece::Rook,
4292 chess::Piece::Bishop,
4293 chess::Piece::Knight,
4294 ] {
4295 material_count += board.pieces(piece).popcnt();
4296 }
4297
4298 match material_count {
4299 0..=4 => GamePhase::Endgame, 5..=12 => GamePhase::Middlegame, _ => GamePhase::Opening, }
4303 }
4304
4305 #[allow(dead_code)]
4307 fn count_king_attackers(&self, board: &Board, color: Color) -> u32 {
4308 let king_square = board.king_square(color);
4309 let opponent_color = if color == Color::White {
4310 Color::Black
4311 } else {
4312 Color::White
4313 };
4314
4315 let mut attackers = 0;
4317
4318 for piece in [
4320 chess::Piece::Queen,
4321 chess::Piece::Rook,
4322 chess::Piece::Bishop,
4323 chess::Piece::Knight,
4324 chess::Piece::Pawn,
4325 ] {
4326 let enemy_pieces = board.pieces(piece) & board.color_combined(opponent_color);
4327
4328 for square in enemy_pieces {
4330 let rank_diff = (king_square.get_rank().to_index() as i32
4331 - square.get_rank().to_index() as i32)
4332 .abs();
4333 let file_diff = (king_square.get_file().to_index() as i32
4334 - square.get_file().to_index() as i32)
4335 .abs();
4336
4337 let is_threat = match piece {
4339 chess::Piece::Queen => rank_diff <= 2 || file_diff <= 2,
4340 chess::Piece::Rook => rank_diff <= 2 || file_diff <= 2,
4341 chess::Piece::Bishop => rank_diff == file_diff && rank_diff <= 2,
4342 chess::Piece::Knight => {
4343 (rank_diff == 2 && file_diff == 1) || (rank_diff == 1 && file_diff == 2)
4344 }
4345 chess::Piece::Pawn => {
4346 rank_diff == 1
4347 && file_diff == 1
4348 && ((color == Color::White
4349 && square.get_rank().to_index()
4350 > king_square.get_rank().to_index())
4351 || (color == Color::Black
4352 && square.get_rank().to_index()
4353 < king_square.get_rank().to_index()))
4354 }
4355 _ => false,
4356 };
4357
4358 if is_threat {
4359 attackers += 1;
4360 }
4361 }
4362 }
4363
4364 attackers
4365 }
4366
4367 fn get_file_mask(&self, file: chess::File) -> chess::BitBoard {
4369 chess::BitBoard(0x0101010101010101u64 << file.to_index())
4370 }
4371
4372 fn evaluate_pawn_structure(&self, board: &Board) -> f32 {
4374 let mut score = 0.0;
4375
4376 for color in [Color::White, Color::Black] {
4377 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
4378 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4379
4380 for file in 0..8 {
4382 let file_mask = self.get_file_mask(chess::File::from_index(file));
4383 let file_pawns = pawns & file_mask;
4384 let pawn_count = file_pawns.popcnt();
4385
4386 if pawn_count > 1 {
4388 score += -0.5 * multiplier * (pawn_count - 1) as f32; }
4390
4391 if pawn_count > 0 {
4393 let has_adjacent_pawns = self.has_adjacent_pawns(board, color, file);
4394 if !has_adjacent_pawns {
4395 score += -0.3 * multiplier; }
4397 }
4398
4399 for square in file_pawns {
4401 if self.is_passed_pawn(board, square, color) {
4403 let rank = square.get_rank().to_index();
4404 let advancement = if color == Color::White {
4405 rank
4406 } else {
4407 7 - rank
4408 };
4409 score += (0.2 + advancement as f32 * 0.3) * multiplier; }
4411
4412 if self.is_backward_pawn(board, square, color) {
4414 score += -0.2 * multiplier;
4415 }
4416
4417 if self.has_pawn_support(board, square, color) {
4419 score += 0.1 * multiplier;
4420 }
4421 }
4422 }
4423
4424 score += self.evaluate_pawn_chains(board, color) * multiplier;
4426 }
4427
4428 score
4429 }
4430
4431 fn has_adjacent_pawns(&self, board: &Board, color: Color, file: usize) -> bool {
4433 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4434
4435 if file > 0 {
4437 let left_file_mask = self.get_file_mask(chess::File::from_index(file - 1));
4438 if (pawns & left_file_mask).popcnt() > 0 {
4439 return true;
4440 }
4441 }
4442
4443 if file < 7 {
4444 let right_file_mask = self.get_file_mask(chess::File::from_index(file + 1));
4445 if (pawns & right_file_mask).popcnt() > 0 {
4446 return true;
4447 }
4448 }
4449
4450 false
4451 }
4452
4453 fn is_passed_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
4455 let opponent_color = if color == Color::White {
4456 Color::Black
4457 } else {
4458 Color::White
4459 };
4460 let opponent_pawns =
4461 board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color);
4462
4463 let file = pawn_square.get_file().to_index();
4464 let rank = pawn_square.get_rank().to_index();
4465
4466 for opponent_square in opponent_pawns {
4468 let opp_file = opponent_square.get_file().to_index();
4469 let opp_rank = opponent_square.get_rank().to_index();
4470
4471 let file_diff = (file as i32 - opp_file as i32).abs();
4473
4474 if file_diff <= 1 {
4475 if color == Color::White && opp_rank > rank {
4477 return false; }
4479 if color == Color::Black && opp_rank < rank {
4480 return false; }
4482 }
4483 }
4484
4485 true
4486 }
4487
4488 fn is_backward_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
4490 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4491 let file = pawn_square.get_file().to_index();
4492 let rank = pawn_square.get_rank().to_index();
4493
4494 for support_file in [file.saturating_sub(1), (file + 1).min(7)] {
4496 if support_file == file {
4497 continue;
4498 }
4499
4500 let file_mask = self.get_file_mask(chess::File::from_index(support_file));
4501 let file_pawns = pawns & file_mask;
4502
4503 for support_square in file_pawns {
4504 let support_rank = support_square.get_rank().to_index();
4505
4506 if color == Color::White && support_rank < rank {
4508 return false; }
4510 if color == Color::Black && support_rank > rank {
4511 return false; }
4513 }
4514 }
4515
4516 true
4517 }
4518
4519 fn has_pawn_support(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
4521 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4522 let file = pawn_square.get_file().to_index();
4523 let rank = pawn_square.get_rank().to_index();
4524
4525 for support_file in [file.saturating_sub(1), (file + 1).min(7)] {
4527 if support_file == file {
4528 continue;
4529 }
4530
4531 let file_mask = self.get_file_mask(chess::File::from_index(support_file));
4532 let file_pawns = pawns & file_mask;
4533
4534 for support_square in file_pawns {
4535 let support_rank = support_square.get_rank().to_index();
4536
4537 if (support_rank as i32 - rank as i32).abs() == 1 {
4539 return true;
4540 }
4541 }
4542 }
4543
4544 false
4545 }
4546
4547 fn evaluate_pawn_chains(&self, board: &Board, color: Color) -> f32 {
4549 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4550 let mut chain_score = 0.0;
4551
4552 let mut chain_lengths = Vec::new();
4554 let mut visited = std::collections::HashSet::new();
4555
4556 for pawn_square in pawns {
4557 if visited.contains(&pawn_square) {
4558 continue;
4559 }
4560
4561 let chain_length = self.count_pawn_chain(board, pawn_square, color, &mut visited);
4562 if chain_length > 1 {
4563 chain_lengths.push(chain_length);
4564 }
4565 }
4566
4567 for &length in &chain_lengths {
4569 chain_score += (length as f32 - 1.0) * 0.15; }
4571
4572 chain_score
4573 }
4574
4575 #[allow(clippy::only_used_in_recursion)]
4577 fn count_pawn_chain(
4578 &self,
4579 board: &Board,
4580 start_square: Square,
4581 color: Color,
4582 visited: &mut std::collections::HashSet<Square>,
4583 ) -> usize {
4584 if visited.contains(&start_square) {
4585 return 0;
4586 }
4587
4588 visited.insert(start_square);
4589 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4590
4591 if (pawns & chess::BitBoard::from_square(start_square)) == chess::BitBoard(0) {
4593 return 0;
4594 }
4595
4596 let mut count = 1;
4597 let file = start_square.get_file().to_index();
4598 let rank = start_square.get_rank().to_index();
4599
4600 for &(file_offset, rank_offset) in &[(-1i32, -1i32), (-1, 1), (1, -1), (1, 1)] {
4602 let new_file = file as i32 + file_offset;
4603 let new_rank = rank as i32 + rank_offset;
4604
4605 if (0..8).contains(&new_file) && (0..8).contains(&new_rank) {
4606 let square_index = (new_rank * 8 + new_file) as u8;
4607 let new_square = unsafe { Square::new(square_index) };
4608 if (pawns & chess::BitBoard::from_square(new_square)) != chess::BitBoard(0)
4609 && !visited.contains(&new_square)
4610 {
4611 count += self.count_pawn_chain(board, new_square, color, visited);
4612 }
4613 }
4614 }
4615
4616 count
4617 }
4618
4619 fn is_tactical_position(&self, board: &Board) -> bool {
4621 if board.checkers().popcnt() > 0 {
4623 return true;
4624 }
4625
4626 let captures = self.generate_captures(board);
4628 if !captures.is_empty() {
4629 return true;
4630 }
4631
4632 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
4634 if legal_moves.len() > 35 {
4635 return true;
4636 }
4637
4638 false
4639 }
4640
4641 fn is_capture_or_promotion(&self, chess_move: &ChessMove, board: &Board) -> bool {
4643 board.piece_on(chess_move.get_dest()).is_some() || chess_move.get_promotion().is_some()
4644 }
4645
4646 fn has_non_pawn_material(&self, board: &Board, color: Color) -> bool {
4648 let pieces = board.color_combined(color)
4649 & !board.pieces(chess::Piece::Pawn)
4650 & !board.pieces(chess::Piece::King);
4651 pieces.popcnt() > 0
4652 }
4653
4654 fn is_killer_move(&self, chess_move: &ChessMove) -> bool {
4656 for depth_killers in &self.killer_moves {
4658 for killer_move in depth_killers.iter().flatten() {
4659 if killer_move == chess_move {
4660 return true;
4661 }
4662 }
4663 }
4664 false
4665 }
4666
4667 fn store_killer_move(&mut self, chess_move: ChessMove, depth: u32) {
4669 let depth_idx = (depth as usize).min(self.killer_moves.len() - 1);
4670
4671 if let Some(first_killer) = self.killer_moves[depth_idx][0] {
4673 if first_killer != chess_move {
4674 self.killer_moves[depth_idx][1] = Some(first_killer);
4675 self.killer_moves[depth_idx][0] = Some(chess_move);
4676 }
4677 } else {
4678 self.killer_moves[depth_idx][0] = Some(chess_move);
4679 }
4680 }
4681
4682 fn update_history(&mut self, chess_move: &ChessMove, depth: u32) {
4684 let key = (chess_move.get_source(), chess_move.get_dest());
4685 let bonus = depth * depth; let current = self.history_heuristic.get(&key).unwrap_or(&0);
4688 self.history_heuristic.insert(key, current + bonus);
4689 }
4690
4691 fn get_history_score(&self, chess_move: &ChessMove) -> u32 {
4693 let key = (chess_move.get_source(), chess_move.get_dest());
4694 *self.history_heuristic.get(&key).unwrap_or(&0)
4695 }
4696
4697 #[allow(dead_code)]
4699 fn store_counter_move(&mut self, refutation: ChessMove) {
4700 if let Some(last_move) = self.last_move {
4701 let last_move_key = (last_move.get_source(), last_move.get_dest());
4702 self.counter_moves.insert(last_move_key, refutation);
4703 }
4704 }
4705
4706 #[allow(dead_code)]
4708 fn update_last_move(&mut self, chess_move: ChessMove) {
4709 self.last_move = Some(chess_move);
4710 }
4711
4712 pub fn clear_cache(&mut self) {
4714 self.transposition_table.clear();
4715 }
4716
4717 pub fn get_stats(&self) -> (u64, usize) {
4719 (self.nodes_searched, self.transposition_table.len())
4720 }
4721
4722 fn evaluate_endgame_patterns(&self, board: &Board) -> f32 {
4724 let mut score = 0.0;
4725
4726 let piece_count = self.count_all_pieces(board);
4728 if piece_count > 10 {
4729 return 0.0; }
4731
4732 let endgame_weight = self.config.endgame_evaluation_weight;
4734
4735 score += self.evaluate_king_pawn_endgames(board) * endgame_weight;
4737 score += self.evaluate_basic_mate_patterns(board) * endgame_weight;
4738 score += self.evaluate_opposition_patterns(board) * endgame_weight;
4739 score += self.evaluate_key_squares(board) * endgame_weight;
4740 score += self.evaluate_zugzwang_patterns(board) * endgame_weight;
4741
4742 score += self.evaluate_piece_coordination_endgame(board) * endgame_weight;
4744 score += self.evaluate_fortress_patterns(board) * endgame_weight;
4745 score += self.evaluate_theoretical_endgames(board) * endgame_weight;
4746
4747 score
4748 }
4749
4750 fn count_all_pieces(&self, board: &Board) -> u32 {
4752 let mut count = 0;
4753 for piece in [
4754 chess::Piece::Pawn,
4755 chess::Piece::Knight,
4756 chess::Piece::Bishop,
4757 chess::Piece::Rook,
4758 chess::Piece::Queen,
4759 ] {
4760 count += board.pieces(piece).popcnt();
4761 }
4762 count += board.pieces(chess::Piece::King).popcnt(); count
4764 }
4765
4766 fn evaluate_king_pawn_endgames(&self, board: &Board) -> f32 {
4768 let mut score = 0.0;
4769
4770 for color in [Color::White, Color::Black] {
4772 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
4773 let king_square = board.king_square(color);
4774 let opponent_king_square = board.king_square(!color);
4775 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4776
4777 for pawn_square in pawns {
4778 if self.is_passed_pawn(board, pawn_square, color) {
4779 let _pawn_file = pawn_square.get_file().to_index();
4780 let pawn_rank = pawn_square.get_rank().to_index();
4781
4782 let promotion_rank = if color == Color::White { 7 } else { 0 };
4784 let promotion_square = Square::make_square(
4785 chess::Rank::from_index(promotion_rank),
4786 chess::File::from_index(_pawn_file),
4787 );
4788
4789 let king_distance = self.square_distance(king_square, promotion_square);
4791 let opponent_king_distance =
4792 self.square_distance(opponent_king_square, promotion_square);
4793 let pawn_distance = (promotion_rank as i32 - pawn_rank as i32).unsigned_abs();
4794
4795 if pawn_distance < opponent_king_distance {
4797 score += 2.0 * multiplier; } else if king_distance < opponent_king_distance {
4799 score += 1.0 * multiplier; }
4801 }
4802 }
4803 }
4804
4805 score
4806 }
4807
4808 fn evaluate_basic_mate_patterns(&self, board: &Board) -> f32 {
4810 let mut score = 0.0;
4811
4812 for color in [Color::White, Color::Black] {
4813 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
4814 let opponent_color = !color;
4815
4816 let queens = (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt();
4817 let rooks = (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt();
4818 let bishops =
4819 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt();
4820 let knights =
4821 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt();
4822
4823 let opp_queens =
4824 (board.pieces(chess::Piece::Queen) & board.color_combined(opponent_color)).popcnt();
4825 let opp_rooks =
4826 (board.pieces(chess::Piece::Rook) & board.color_combined(opponent_color)).popcnt();
4827 let opp_bishops = (board.pieces(chess::Piece::Bishop)
4828 & board.color_combined(opponent_color))
4829 .popcnt();
4830 let opp_knights = (board.pieces(chess::Piece::Knight)
4831 & board.color_combined(opponent_color))
4832 .popcnt();
4833 let opp_pawns =
4834 (board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color)).popcnt();
4835
4836 if opp_queens == 0
4838 && opp_rooks == 0
4839 && opp_bishops == 0
4840 && opp_knights == 0
4841 && opp_pawns == 0
4842 {
4843 if queens > 0 || rooks > 0 {
4845 let king_square = board.king_square(color);
4847 let opponent_king_square = board.king_square(opponent_color);
4848 let corner_distance = self.distance_to_nearest_corner(opponent_king_square);
4849 let king_distance = self.square_distance(king_square, opponent_king_square);
4850
4851 score += 1.0 * multiplier; score += (7.0 - corner_distance as f32) * 0.1 * multiplier; score += (8.0 - king_distance as f32) * 0.05 * multiplier; }
4855
4856 if bishops >= 2 {
4857 let opponent_king_square = board.king_square(opponent_color);
4859 let corner_distance = self.distance_to_nearest_corner(opponent_king_square);
4860 score += 0.8 * multiplier; score += (7.0 - corner_distance as f32) * 0.08 * multiplier;
4862 }
4863
4864 if bishops >= 1 && knights >= 1 {
4865 score += 0.6 * multiplier; }
4868 }
4869 }
4870
4871 score
4872 }
4873
4874 fn evaluate_opposition_patterns(&self, board: &Board) -> f32 {
4876 let mut score = 0.0;
4877
4878 let white_king = board.king_square(Color::White);
4879 let black_king = board.king_square(Color::Black);
4880
4881 let file_diff = (white_king.get_file().to_index() as i32
4882 - black_king.get_file().to_index() as i32)
4883 .abs();
4884 let rank_diff = (white_king.get_rank().to_index() as i32
4885 - black_king.get_rank().to_index() as i32)
4886 .abs();
4887
4888 if (file_diff == 0 && rank_diff == 2) || (file_diff == 2 && rank_diff == 0) {
4890 let opposition_bonus = 0.2;
4892 if board.side_to_move() == Color::White {
4893 score -= opposition_bonus; } else {
4895 score += opposition_bonus; }
4897 }
4898
4899 if file_diff == 0 && rank_diff % 2 == 0 && rank_diff > 2 {
4901 let distant_opposition_bonus = 0.1;
4902 if board.side_to_move() == Color::White {
4903 score -= distant_opposition_bonus;
4904 } else {
4905 score += distant_opposition_bonus;
4906 }
4907 }
4908
4909 score
4910 }
4911
4912 fn evaluate_key_squares(&self, board: &Board) -> f32 {
4914 let mut score = 0.0;
4915
4916 for color in [Color::White, Color::Black] {
4918 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
4919 let king_square = board.king_square(color);
4920 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
4921
4922 for pawn_square in pawns {
4923 if self.is_passed_pawn(board, pawn_square, color) {
4924 let key_squares = self.get_key_squares(pawn_square, color);
4926
4927 for key_square in key_squares {
4928 let distance = self.square_distance(king_square, key_square);
4929 if distance <= 1 {
4930 score += 0.3 * multiplier; } else if distance <= 2 {
4932 score += 0.1 * multiplier; }
4934 }
4935 }
4936 }
4937 }
4938
4939 score
4940 }
4941
4942 fn evaluate_zugzwang_patterns(&self, board: &Board) -> f32 {
4944 let mut score = 0.0;
4945
4946 let piece_count = self.count_all_pieces(board);
4948 if piece_count <= 6 {
4949 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
4951
4952 if legal_moves.len() <= 3 {
4954 let current_eval = self.quick_evaluate_position(board);
4956 let mut bad_moves = 0;
4957
4958 for chess_move in legal_moves.iter().take(3) {
4959 let new_board = board.make_move_new(*chess_move);
4960 let new_eval = -self.quick_evaluate_position(&new_board); if new_eval < current_eval - 0.5 {
4963 bad_moves += 1;
4964 }
4965 }
4966
4967 if bad_moves >= legal_moves.len() / 2 {
4969 let zugzwang_penalty = 0.3;
4970 if board.side_to_move() == Color::White {
4971 score -= zugzwang_penalty;
4972 } else {
4973 score += zugzwang_penalty;
4974 }
4975 }
4976 }
4977 }
4978
4979 score
4980 }
4981
4982 fn square_distance(&self, sq1: Square, sq2: Square) -> u32 {
4984 let file1 = sq1.get_file().to_index() as i32;
4985 let rank1 = sq1.get_rank().to_index() as i32;
4986 let file2 = sq2.get_file().to_index() as i32;
4987 let rank2 = sq2.get_rank().to_index() as i32;
4988
4989 ((file1 - file2).abs() + (rank1 - rank2).abs()) as u32
4990 }
4991
4992 fn distance_to_nearest_corner(&self, square: Square) -> u32 {
4994 let file = square.get_file().to_index() as i32;
4995 let rank = square.get_rank().to_index() as i32;
4996
4997 let corner_distances = [
4998 file + rank, (7 - file) + rank, file + (7 - rank), (7 - file) + (7 - rank), ];
5003
5004 *corner_distances.iter().min().unwrap() as u32
5005 }
5006
5007 fn get_key_squares(&self, pawn_square: Square, color: Color) -> Vec<Square> {
5009 let mut key_squares = Vec::new();
5010 let file = pawn_square.get_file().to_index();
5011 let rank = pawn_square.get_rank().to_index();
5012
5013 let key_rank = if color == Color::White {
5015 if rank + 2 <= 7 {
5016 rank + 2
5017 } else {
5018 return key_squares;
5019 }
5020 } else if rank >= 2 {
5021 rank - 2
5022 } else {
5023 return key_squares;
5024 };
5025
5026 for key_file in (file.saturating_sub(1))..=(file + 1).min(7) {
5028 let square = Square::make_square(
5029 chess::Rank::from_index(key_rank),
5030 chess::File::from_index(key_file),
5031 );
5032 key_squares.push(square);
5033 }
5034
5035 key_squares
5036 }
5037
5038 fn quick_evaluate_position(&self, board: &Board) -> f32 {
5040 let mut score = 0.0;
5041
5042 score += self.material_balance(board);
5044
5045 for color in [Color::White, Color::Black] {
5047 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
5048 let king_square = board.king_square(color);
5049 let file = king_square.get_file().to_index();
5050 let rank = king_square.get_rank().to_index();
5051
5052 let center_distance = (file as f32 - 3.5).abs() + (rank as f32 - 3.5).abs();
5054 score += (7.0 - center_distance) * 0.05 * multiplier;
5055 }
5056
5057 score
5058 }
5059
5060 fn evaluate_piece_coordination_endgame(&self, board: &Board) -> f32 {
5062 let mut score = 0.0;
5063
5064 for color in [Color::White, Color::Black] {
5065 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
5066 let king_square = board.king_square(color);
5067
5068 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
5070 for rook_square in rooks {
5071 let distance = self.square_distance(king_square, rook_square);
5072 if distance <= 3 {
5073 score += 0.2 * multiplier; }
5075
5076 let rook_rank = rook_square.get_rank().to_index();
5078 if (color == Color::White && rook_rank == 6)
5079 || (color == Color::Black && rook_rank == 1)
5080 {
5081 score += 0.4 * multiplier;
5082 }
5083 }
5084
5085 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
5087 for queen_square in queens {
5088 let distance = self.square_distance(king_square, queen_square);
5089 if distance <= 4 {
5090 score += 0.15 * multiplier; }
5092 }
5093
5094 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
5096 if bishops.popcnt() >= 2 {
5097 score += 0.3 * multiplier; }
5099 }
5100
5101 score
5102 }
5103
5104 fn evaluate_fortress_patterns(&self, board: &Board) -> f32 {
5106 let mut score = 0.0;
5107
5108 for color in [Color::White, Color::Black] {
5110 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
5111 let opponent_color = !color;
5112
5113 let material_diff = self.calculate_material_difference(board, color);
5115
5116 if material_diff < -2.0 {
5118 let king_square = board.king_square(color);
5120 let king_file = king_square.get_file().to_index();
5121 let king_rank = king_square.get_rank().to_index();
5122
5123 if (king_file <= 1 || king_file >= 6) && (king_rank <= 1 || king_rank >= 6) {
5125 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
5126 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
5127
5128 if bishops.popcnt() > 0 && pawns.popcnt() >= 2 {
5130 score += 0.5 * multiplier; }
5132 }
5133
5134 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
5136 let opp_pawns =
5137 board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color);
5138 if rooks.popcnt() > 0 && opp_pawns.popcnt() >= 3 {
5139 score += 0.3 * multiplier; }
5141 }
5142 }
5143
5144 score
5145 }
5146
5147 fn evaluate_theoretical_endgames(&self, board: &Board) -> f32 {
5149 let mut score = 0.0;
5150
5151 let piece_count = self.count_all_pieces(board);
5152
5153 if piece_count <= 6 {
5155 score += self.evaluate_rook_endgames(board);
5157
5158 score += self.evaluate_bishop_endgames(board);
5160
5161 score += self.evaluate_knight_endgames(board);
5163
5164 score += self.evaluate_mixed_piece_endgames(board);
5166 }
5167
5168 score
5169 }
5170
5171 fn evaluate_rook_endgames(&self, board: &Board) -> f32 {
5173 let mut score = 0.0;
5174
5175 for color in [Color::White, Color::Black] {
5176 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
5177 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
5178 let opponent_king = board.king_square(!color);
5179
5180 for rook_square in rooks {
5181 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
5183 for pawn_square in pawns {
5184 if self.is_passed_pawn(board, pawn_square, color) {
5185 let rook_file = rook_square.get_file().to_index();
5186 let pawn_file = pawn_square.get_file().to_index();
5187 let rook_rank = rook_square.get_rank().to_index();
5188 let pawn_rank = pawn_square.get_rank().to_index();
5189
5190 if rook_file == pawn_file
5192 && ((color == Color::White && rook_rank < pawn_rank)
5193 || (color == Color::Black && rook_rank > pawn_rank))
5194 {
5195 score += 0.6 * multiplier; }
5197 }
5198 }
5199
5200 let king_distance_to_rook = self.square_distance(opponent_king, rook_square);
5202 if king_distance_to_rook >= 4 {
5203 score += 0.2 * multiplier; }
5205
5206 let rook_file = rook_square.get_file().to_index();
5208 if self.is_file_open(board, rook_file) {
5209 score += 0.3 * multiplier; }
5211 }
5212 }
5213
5214 score
5215 }
5216
5217 fn evaluate_bishop_endgames(&self, board: &Board) -> f32 {
5219 let mut score = 0.0;
5220
5221 for color in [Color::White, Color::Black] {
5222 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
5223 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
5224 let opponent_color = !color;
5225
5226 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
5228 for pawn_square in pawns {
5229 let pawn_file = pawn_square.get_file().to_index();
5230
5231 if pawn_file == 0 || pawn_file == 7 {
5233 for bishop_square in bishops {
5234 let promotion_square = if color == Color::White {
5235 Square::make_square(
5236 chess::Rank::Eighth,
5237 chess::File::from_index(pawn_file),
5238 )
5239 } else {
5240 Square::make_square(
5241 chess::Rank::First,
5242 chess::File::from_index(pawn_file),
5243 )
5244 };
5245
5246 if self.bishop_attacks_square(board, bishop_square, promotion_square) {
5248 score += 0.4 * multiplier; } else {
5250 score -= 0.8 * multiplier; }
5252 }
5253 }
5254 }
5255
5256 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(opponent_color);
5258 if bishops.popcnt() > 0 && knights.popcnt() > 0 {
5259 let pawns_kingside = self.count_pawns_on_side(board, true);
5260 let pawns_queenside = self.count_pawns_on_side(board, false);
5261
5262 if pawns_kingside == 0 || pawns_queenside == 0 {
5263 score += 0.25 * multiplier; }
5265 }
5266 }
5267
5268 score
5269 }
5270
5271 fn evaluate_knight_endgames(&self, board: &Board) -> f32 {
5273 let mut score = 0.0;
5274
5275 for color in [Color::White, Color::Black] {
5276 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
5277 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
5278
5279 for knight_square in knights {
5280 let file = knight_square.get_file().to_index();
5282 let rank = knight_square.get_rank().to_index();
5283 let center_distance = ((file as f32 - 3.5).abs() + (rank as f32 - 3.5).abs()) / 2.0;
5284 score += (4.0 - center_distance) * 0.1 * multiplier;
5285
5286 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
5288 for pawn_square in pawns {
5289 if self.is_passed_pawn(board, pawn_square, color) {
5290 let distance = self.square_distance(knight_square, pawn_square);
5291 if distance <= 2 {
5292 score += 0.3 * multiplier; }
5294 }
5295 }
5296 }
5297 }
5298
5299 score
5300 }
5301
5302 fn evaluate_mixed_piece_endgames(&self, board: &Board) -> f32 {
5304 let mut score = 0.0;
5305
5306 for color in [Color::White, Color::Black] {
5307 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
5308
5309 let queens = (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt();
5310 let rooks = (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt();
5311 let bishops =
5312 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt();
5313 let knights =
5314 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt();
5315
5316 if queens > 0 && rooks == 0 {
5318 let opponent_color = !color;
5319 let opp_rooks = (board.pieces(chess::Piece::Rook)
5320 & board.color_combined(opponent_color))
5321 .popcnt();
5322 let opp_minors = (board.pieces(chess::Piece::Bishop)
5323 & board.color_combined(opponent_color))
5324 .popcnt()
5325 + (board.pieces(chess::Piece::Knight) & board.color_combined(opponent_color))
5326 .popcnt();
5327
5328 if opp_rooks > 0 && opp_minors > 0 {
5329 score += 0.5 * multiplier; }
5331 }
5332
5333 if rooks > 0 && bishops > 0 && knights == 0 {
5335 let opponent_color = !color;
5336 let opp_rooks = (board.pieces(chess::Piece::Rook)
5337 & board.color_combined(opponent_color))
5338 .popcnt();
5339 let opp_knights = (board.pieces(chess::Piece::Knight)
5340 & board.color_combined(opponent_color))
5341 .popcnt();
5342
5343 if opp_rooks > 0 && opp_knights > 0 {
5344 score += 0.2 * multiplier; }
5346 }
5347 }
5348
5349 score
5350 }
5351
5352 fn calculate_material_difference(&self, board: &Board, color: Color) -> f32 {
5354 let opponent_color = !color;
5355
5356 let my_material = self.calculate_total_material(board, color);
5357 let opp_material = self.calculate_total_material(board, opponent_color);
5358
5359 my_material - opp_material
5360 }
5361
5362 fn calculate_total_material(&self, board: &Board, color: Color) -> f32 {
5364 let mut material = 0.0;
5365
5366 material +=
5367 (board.pieces(chess::Piece::Pawn) & board.color_combined(color)).popcnt() as f32 * 1.0;
5368 material += (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt()
5369 as f32
5370 * 3.0;
5371 material += (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt()
5372 as f32
5373 * 3.0;
5374 material +=
5375 (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt() as f32 * 5.0;
5376 material +=
5377 (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt() as f32 * 9.0;
5378
5379 material
5380 }
5381
5382 fn bishop_attacks_square(
5384 &self,
5385 board: &Board,
5386 bishop_square: Square,
5387 target_square: Square,
5388 ) -> bool {
5389 let file_diff = (bishop_square.get_file().to_index() as i32
5390 - target_square.get_file().to_index() as i32)
5391 .abs();
5392 let rank_diff = (bishop_square.get_rank().to_index() as i32
5393 - target_square.get_rank().to_index() as i32)
5394 .abs();
5395
5396 if file_diff == rank_diff {
5398 let file_step =
5400 if target_square.get_file().to_index() > bishop_square.get_file().to_index() {
5401 1
5402 } else {
5403 -1
5404 };
5405 let rank_step =
5406 if target_square.get_rank().to_index() > bishop_square.get_rank().to_index() {
5407 1
5408 } else {
5409 -1
5410 };
5411
5412 let mut current_file = bishop_square.get_file().to_index() as i32 + file_step;
5413 let mut current_rank = bishop_square.get_rank().to_index() as i32 + rank_step;
5414
5415 while current_file != target_square.get_file().to_index() as i32 {
5416 let square = Square::make_square(
5417 chess::Rank::from_index(current_rank as usize),
5418 chess::File::from_index(current_file as usize),
5419 );
5420
5421 if board.piece_on(square).is_some() {
5422 return false; }
5424
5425 current_file += file_step;
5426 current_rank += rank_step;
5427 }
5428
5429 true
5430 } else {
5431 false
5432 }
5433 }
5434
5435 fn count_pawns_on_side(&self, board: &Board, kingside: bool) -> u32 {
5437 let mut count = 0;
5438 let pawns = board.pieces(chess::Piece::Pawn);
5439
5440 for pawn_square in pawns.into_iter() {
5441 let file = pawn_square.get_file().to_index();
5442 if (kingside && file >= 4) || (!kingside && file < 4) {
5443 count += 1;
5444 }
5445 }
5446
5447 count
5448 }
5449
5450 fn is_file_open(&self, board: &Board, file: usize) -> bool {
5452 let file_mask = self.get_file_mask(chess::File::from_index(file));
5453 let pawns = board.pieces(chess::Piece::Pawn);
5454 (pawns & file_mask).popcnt() == 0
5455 }
5456
5457 fn count_attackers(&self, board: &Board, square: Square, color: Color) -> usize {
5459 let mut count = 0;
5460
5461 let pawn_attacks = chess::get_pawn_attacks(square, !color, chess::BitBoard::new(0));
5463 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
5464 count += (pawn_attacks & pawns).popcnt() as usize;
5465
5466 let knight_attacks = chess::get_knight_moves(square);
5468 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
5469 count += (knight_attacks & knights).popcnt() as usize;
5470
5471 let king_attacks = chess::get_king_moves(square);
5473 let kings = board.pieces(chess::Piece::King) & board.color_combined(color);
5474 count += (king_attacks & kings).popcnt() as usize;
5475
5476 let all_pieces = *board.combined();
5478
5479 let bishop_attacks = chess::get_bishop_moves(square, all_pieces);
5481 let bishops_queens = (board.pieces(chess::Piece::Bishop)
5482 | board.pieces(chess::Piece::Queen))
5483 & board.color_combined(color);
5484 count += (bishop_attacks & bishops_queens).popcnt() as usize;
5485
5486 let rook_attacks = chess::get_rook_moves(square, all_pieces);
5488 let rooks_queens = (board.pieces(chess::Piece::Rook) | board.pieces(chess::Piece::Queen))
5489 & board.color_combined(color);
5490 count += (rook_attacks & rooks_queens).popcnt() as usize;
5491
5492 count
5493 }
5494
5495 fn evaluate_hanging_pieces(&self, board: &Board) -> f32 {
5497 let mut hanging_penalty = 0.0;
5498
5499 for color in [Color::White, Color::Black] {
5501 let multiplier = if color == Color::White { -1.0 } else { 1.0 }; for piece_type in [
5505 chess::Piece::Queen,
5506 chess::Piece::Rook,
5507 chess::Piece::Bishop,
5508 chess::Piece::Knight,
5509 chess::Piece::Pawn,
5510 ] {
5511 let pieces = board.pieces(piece_type) & board.color_combined(color);
5512
5513 for square in pieces {
5514 if piece_type == chess::Piece::King {
5516 continue;
5517 }
5518
5519 let our_defenders = self.count_attackers(board, square, color);
5520 let enemy_attackers = self.count_attackers(board, square, !color);
5521
5522 if enemy_attackers > 0 && our_defenders == 0 {
5524 let piece_value = self.get_piece_value(piece_type) as f32;
5525 hanging_penalty += piece_value * multiplier * 0.8; }
5527 else if enemy_attackers > our_defenders && enemy_attackers > 0 {
5529 let piece_value = self.get_piece_value(piece_type) as f32;
5530 hanging_penalty += piece_value * multiplier * 0.3; }
5532 }
5533 }
5534 }
5535
5536 hanging_penalty
5537 }
5538
5539 fn find_mate_in_n(&self, board: &Board, max_depth: u32) -> Option<ChessMove> {
5541 if max_depth == 0 {
5542 return None;
5543 }
5544
5545 let moves = MoveGen::new_legal(board);
5546
5547 for chess_move in moves {
5548 let new_board = board.make_move_new(chess_move);
5549
5550 if new_board.status() == chess::BoardStatus::Checkmate {
5552 return Some(chess_move);
5553 }
5554
5555 if max_depth > 1 && self.is_forced_mate(&new_board, max_depth - 1, false) {
5557 return Some(chess_move);
5558 }
5559 }
5560
5561 None
5562 }
5563
5564 fn is_forced_mate(&self, board: &Board, depth: u32, maximizing: bool) -> bool {
5566 if depth == 0 {
5567 return false;
5568 }
5569
5570 if board.status() == chess::BoardStatus::Checkmate {
5571 return !maximizing; }
5573
5574 if board.status() != chess::BoardStatus::Ongoing {
5575 return false; }
5577
5578 let moves = MoveGen::new_legal(board);
5579 let move_count = moves.len();
5580
5581 if move_count > 20 {
5583 return false; }
5585
5586 if maximizing {
5587 let mut forcing_moves = Vec::new();
5589 let mut other_moves = Vec::new();
5590
5591 for chess_move in moves {
5592 let new_board = board.make_move_new(chess_move);
5593 if new_board.checkers().popcnt() > 0
5594 || board.piece_on(chess_move.get_dest()).is_some()
5595 {
5596 forcing_moves.push(chess_move); } else {
5598 other_moves.push(chess_move);
5599 }
5600 }
5601
5602 for chess_move in forcing_moves {
5604 let new_board = board.make_move_new(chess_move);
5605 if self.is_forced_mate(&new_board, depth - 1, false) {
5606 return true;
5607 }
5608 }
5609
5610 if other_moves.len() <= 3 {
5612 for chess_move in other_moves {
5613 let new_board = board.make_move_new(chess_move);
5614 if self.is_forced_mate(&new_board, depth - 1, false) {
5615 return true;
5616 }
5617 }
5618 }
5619
5620 false
5621 } else {
5622 if move_count > 10 {
5624 return false; }
5626
5627 for chess_move in moves {
5628 let new_board = board.make_move_new(chess_move);
5629 if !self.is_forced_mate(&new_board, depth - 1, true) {
5630 return false; }
5632 }
5633 true }
5635 }
5636
5637 fn has_tactical_threats(&self, board: &Board) -> bool {
5639 if board.checkers().popcnt() > 0 {
5641 return true; }
5643
5644 let moves = MoveGen::new_legal(board);
5646 let capture_count = moves
5647 .filter(|m| board.piece_on(m.get_dest()).is_some())
5648 .count();
5649
5650 capture_count > 3 }
5652
5653 fn evaluate_king_safety(&self, board: &Board) -> f32 {
5655 let mut safety_score = 0.0;
5656
5657 for color in [Color::White, Color::Black] {
5658 let king_square = board.king_square(color);
5659 let multiplier = if color == board.side_to_move() {
5660 1.0
5661 } else {
5662 -1.0
5663 };
5664
5665 let attackers = self.count_attackers(board, king_square, !color);
5667 let defenders = self.count_attackers(board, king_square, color);
5668
5669 if attackers > defenders {
5670 safety_score -= (attackers - defenders) as f32 * 2.0 * multiplier;
5671 }
5672
5673 let king_file = king_square.get_file().to_index() as i8;
5675 let king_rank = king_square.get_rank().to_index() as i8;
5676
5677 if (king_file >= 2 && king_file <= 5) && (king_rank >= 2 && king_rank <= 5) {
5678 safety_score -= 8.0 * multiplier; }
5680
5681 let expected_rank = if color == Color::White { 0 } else { 7 };
5683 let rank_distance = (king_rank - expected_rank).abs();
5684 if rank_distance > 1 {
5685 safety_score -= rank_distance as f32 * 5.0 * multiplier; }
5687
5688 if rank_distance == 1 && (king_file == 4) {
5690 safety_score -= 10.0 * multiplier; }
5693
5694 if attackers >= 2 {
5696 safety_score -= 5.0 * multiplier; }
5698
5699 let pawn_shield_score = self.evaluate_king_pawn_shield(board, king_square, color);
5701 safety_score += pawn_shield_score * multiplier;
5702 }
5703
5704 safety_score
5705 }
5706
5707 fn evaluate_king_pawn_shield(&self, board: &Board, king_square: Square, color: Color) -> f32 {
5709 let mut shield_score = 0.0;
5710 let king_file = king_square.get_file().to_index() as i8;
5711 let king_rank = king_square.get_rank().to_index() as i8;
5712 let direction = if color == Color::White { 1 } else { -1 };
5713
5714 for file_offset in [-1, 0, 1] {
5716 let shield_file = king_file + file_offset;
5717 if shield_file >= 0 && shield_file < 8 {
5718 for rank_offset in [1, 2] {
5719 let shield_rank = king_rank + (direction * rank_offset);
5720 if shield_rank >= 0 && shield_rank < 8 {
5721 let shield_square = Square::make_square(
5722 chess::Rank::from_index(shield_rank as usize),
5723 chess::File::from_index(shield_file as usize),
5724 );
5725
5726 if let Some(piece) = board.piece_on(shield_square) {
5727 if piece == chess::Piece::Pawn
5728 && board.color_on(shield_square) == Some(color)
5729 {
5730 shield_score += 1.0; }
5732 }
5733 }
5734 }
5735 }
5736 }
5737
5738 shield_score
5739 }
5740
5741 fn evaluate_material_safety(&self, board: &Board) -> f32 {
5743 let mut safety_score = 0.0;
5744
5745 for color in [Color::White, Color::Black] {
5747 let multiplier = if color == Color::White { -1.0 } else { 1.0 };
5748
5749 for piece_type in [
5751 chess::Piece::Queen,
5752 chess::Piece::Rook,
5753 chess::Piece::Bishop,
5754 chess::Piece::Knight,
5755 ] {
5756 let pieces = board.pieces(piece_type) & board.color_combined(color);
5757
5758 for square in pieces {
5759 let attackers = self.count_attackers(board, square, !color);
5760 let defenders = self.count_attackers(board, square, color);
5761
5762 if attackers > 0 {
5764 let piece_value = self.get_piece_value(piece_type) as f32;
5765
5766 if defenders == 0 {
5767 safety_score += piece_value * multiplier * 1.2; } else if attackers > defenders {
5770 safety_score += piece_value * multiplier * 0.6; }
5773 }
5774 }
5775 }
5776
5777 for piece_type in [
5780 chess::Piece::Queen,
5781 chess::Piece::Rook,
5782 chess::Piece::Bishop,
5783 chess::Piece::Knight,
5784 ] {
5785 let pieces = board.pieces(piece_type) & board.color_combined(color);
5786
5787 for square in pieces {
5788 let piece_value = self.get_piece_value(piece_type);
5790 let attackers = self.count_attackers(board, square, !color);
5791
5792 if attackers > 0 {
5793 for attacker_square in chess::ALL_SQUARES {
5795 if let Some(attacker_piece) = board.piece_on(attacker_square) {
5796 if board.color_on(attacker_square) == Some(!color) {
5797 let attacker_value = self.get_piece_value(attacker_piece);
5798 if attacker_value < piece_value
5799 && self.can_attack(board, attacker_square, square)
5800 {
5801 safety_score += (piece_value - attacker_value) as f32
5803 * multiplier
5804 * 0.3;
5805 }
5806 }
5807 }
5808 }
5809 }
5810 }
5811 }
5812 }
5813
5814 safety_score
5815 }
5816
5817 fn evaluate_sacrifice_compensation(&self, chess_move: &ChessMove, board: &Board) -> f32 {
5819 let mut compensation = 0.0;
5820 let test_board = board.make_move_new(*chess_move);
5821
5822 if test_board.checkers().popcnt() > 0 {
5824 compensation += 100.0; if test_board.status() == chess::BoardStatus::Checkmate {
5828 compensation += 10000.0; }
5830 }
5831
5832 let our_developed_before = self.count_developed_pieces(board, board.side_to_move());
5834 let our_developed_after = self.count_developed_pieces(&test_board, board.side_to_move());
5835 compensation += (our_developed_after - our_developed_before) as f32 * 50.0;
5836
5837 let enemy_king_safety_before =
5839 self.evaluate_king_safety_for_color(board, !board.side_to_move());
5840 let enemy_king_safety_after =
5841 self.evaluate_king_safety_for_color(&test_board, !board.side_to_move());
5842 let king_safety_improvement = enemy_king_safety_before - enemy_king_safety_after;
5843 compensation += king_safety_improvement * 0.5; let our_activity_before = self.evaluate_piece_activity(board, board.side_to_move());
5847 let our_activity_after = self.evaluate_piece_activity(&test_board, board.side_to_move());
5848 compensation += (our_activity_after - our_activity_before) * 0.3;
5849
5850 compensation
5851 }
5852
5853 fn count_developed_pieces(&self, board: &Board, color: Color) -> u32 {
5855 let mut developed = 0;
5856 let back_rank = if color == Color::White { 0 } else { 7 };
5857
5858 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
5860 for square in knights {
5861 if square.get_rank().to_index() != back_rank {
5862 developed += 1;
5863 }
5864 }
5865
5866 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
5868 for square in bishops {
5869 if square.get_rank().to_index() != back_rank {
5870 developed += 1;
5871 }
5872 }
5873
5874 developed
5875 }
5876
5877 fn evaluate_king_safety_for_color(&self, board: &Board, color: Color) -> f32 {
5879 let king_square = board.king_square(color);
5880 let enemy_attackers = self.count_attackers(board, king_square, !color);
5881 -(enemy_attackers as f32 * 50.0) }
5883
5884 fn evaluate_piece_activity(&self, board: &Board, color: Color) -> f32 {
5886 let mut activity = 0.0;
5887
5888 let center_squares = [
5890 chess::Square::make_square(chess::Rank::Fourth, chess::File::D),
5891 chess::Square::make_square(chess::Rank::Fourth, chess::File::E),
5892 chess::Square::make_square(chess::Rank::Fifth, chess::File::D),
5893 chess::Square::make_square(chess::Rank::Fifth, chess::File::E),
5894 ];
5895
5896 for &square in ¢er_squares {
5897 if let Some(piece) = board.piece_on(square) {
5898 if board.color_on(square) == Some(color) {
5899 let piece_value = match piece {
5900 chess::Piece::Pawn => 30.0,
5901 chess::Piece::Knight => 40.0,
5902 chess::Piece::Bishop => 35.0,
5903 _ => 20.0,
5904 };
5905 activity += piece_value;
5906 }
5907 }
5908 }
5909
5910 activity
5911 }
5912
5913 fn get_nnue_evaluation(&self, board: &Board) -> (f32, f32) {
5915 let material_count = self.count_material(board);
5919
5920 let confidence = if material_count > 20 {
5922 0.5 } else if material_count > 12 {
5924 0.7 } else {
5926 0.8 };
5928
5929 let material_eval = self.material_balance(board) / 100.0;
5931 let king_safety_eval = self.king_safety(board) / 100.0;
5932
5933 let eval = material_eval * 0.7 + king_safety_eval * 0.3;
5935
5936 let clamped_eval = eval.clamp(-5.0, 5.0);
5938
5939 (clamped_eval, confidence)
5940 }
5941
5942 fn evaluate_tactical_patterns_nnue(&self, board: &Board) -> f32 {
5944 let mut tactical_score = 0.0;
5945
5946 for color in [Color::White, Color::Black] {
5948 let multiplier = if color == board.side_to_move() {
5949 1.0
5950 } else {
5951 -1.0
5952 };
5953
5954 let pins_created = self.count_pins_created_by_color(board, color);
5956 tactical_score += pins_created as f32 * 0.3 * multiplier;
5957
5958 let fork_potential = self.count_fork_potential(board, color);
5960 tactical_score += fork_potential as f32 * 0.2 * multiplier;
5961
5962 let discovered_attacks = self.count_discovered_attack_potential(board, color);
5964 tactical_score += discovered_attacks as f32 * 0.25 * multiplier;
5965 }
5966
5967 if board.checkers().popcnt() > 0 {
5969 let moving_color = board.side_to_move();
5970 let king_square = board.king_square(moving_color);
5971 let escape_squares = self.count_king_escape_squares(board, king_square);
5972
5973 tactical_score -= 0.5 + (3.0 - escape_squares as f32) * 0.2;
5975 }
5976
5977 tactical_score
5978 }
5979
5980 fn evaluate_positional_factors_nnue(&self, board: &Board) -> f32 {
5982 let mut positional_score = 0.0;
5983
5984 let center_control = self.evaluate_center_control_detailed(board);
5986 positional_score += center_control * 0.1;
5987
5988 let white_activity = self.evaluate_piece_activity(board, Color::White);
5990 let black_activity = self.evaluate_piece_activity(board, Color::Black);
5991 let activity_diff = (white_activity - black_activity) / 100.0;
5992
5993 if board.side_to_move() == Color::White {
5994 positional_score += activity_diff * 0.15;
5995 } else {
5996 positional_score -= activity_diff * 0.15;
5997 }
5998
5999 let pawn_structure_score = self.evaluate_pawn_structure_nnue(board);
6001 positional_score += pawn_structure_score;
6002
6003 positional_score
6004 }
6005
6006 fn evaluate_development_nnue(&self, board: &Board) -> f32 {
6008 if !self.is_opening_phase(board) {
6009 return 0.0; }
6011
6012 let white_dev = self.count_developed_pieces(board, Color::White);
6013 let black_dev = self.count_developed_pieces(board, Color::Black);
6014 let dev_diff = (white_dev as f32 - black_dev as f32) * 0.15;
6015
6016 if board.side_to_move() == Color::White {
6017 dev_diff
6018 } else {
6019 -dev_diff
6020 }
6021 }
6022
6023 fn count_pins_created_by_color(&self, board: &Board, color: Color) -> u8 {
6025 let mut pin_count = 0;
6026 let enemy_color = !color;
6027 let enemy_king_square = board.king_square(enemy_color);
6028
6029 let pieces = board.color_combined(color);
6031 for piece_square in *pieces {
6032 if let Some(piece) = board.piece_on(piece_square) {
6033 match piece {
6034 chess::Piece::Bishop | chess::Piece::Rook | chess::Piece::Queen => {
6035 if self.creates_pin_on_king(board, piece_square, enemy_king_square, piece) {
6036 pin_count += 1;
6037 }
6038 }
6039 _ => {}
6040 }
6041 }
6042 }
6043
6044 pin_count
6045 }
6046
6047 fn count_fork_potential(&self, board: &Board, color: Color) -> u8 {
6049 let mut fork_count = 0;
6050 let pieces = board.color_combined(color);
6051
6052 for piece_square in *pieces {
6053 if let Some(piece) = board.piece_on(piece_square) {
6054 match piece {
6055 chess::Piece::Knight => {
6056 if self.knight_can_fork(board, piece_square, color) {
6057 fork_count += 1;
6058 }
6059 }
6060 chess::Piece::Pawn => {
6061 if self.pawn_can_fork(board, piece_square, color) {
6062 fork_count += 1;
6063 }
6064 }
6065 _ => {}
6066 }
6067 }
6068 }
6069
6070 fork_count
6071 }
6072
6073 fn count_discovered_attack_potential(&self, board: &Board, color: Color) -> u8 {
6075 let mut discovered_count = 0;
6076 let enemy_king_square = board.king_square(!color);
6077
6078 let pieces = board.color_combined(color);
6080 for piece_square in *pieces {
6081 if self.can_create_discovered_attack(board, piece_square, enemy_king_square) {
6082 discovered_count += 1;
6083 }
6084 }
6085
6086 discovered_count
6087 }
6088
6089 fn evaluate_center_control_detailed(&self, board: &Board) -> f32 {
6091 let center_squares = [
6092 chess::Square::make_square(chess::Rank::Fourth, chess::File::D),
6093 chess::Square::make_square(chess::Rank::Fourth, chess::File::E),
6094 chess::Square::make_square(chess::Rank::Fifth, chess::File::D),
6095 chess::Square::make_square(chess::Rank::Fifth, chess::File::E),
6096 ];
6097
6098 let extended_center = [
6099 chess::Square::make_square(chess::Rank::Third, chess::File::C),
6100 chess::Square::make_square(chess::Rank::Third, chess::File::D),
6101 chess::Square::make_square(chess::Rank::Third, chess::File::E),
6102 chess::Square::make_square(chess::Rank::Third, chess::File::F),
6103 chess::Square::make_square(chess::Rank::Sixth, chess::File::C),
6104 chess::Square::make_square(chess::Rank::Sixth, chess::File::D),
6105 chess::Square::make_square(chess::Rank::Sixth, chess::File::E),
6106 chess::Square::make_square(chess::Rank::Sixth, chess::File::F),
6107 ];
6108
6109 let mut control_score = 0.0;
6110
6111 for square in center_squares {
6113 let white_attackers = self.count_attackers(board, square, Color::White);
6114 let black_attackers = self.count_attackers(board, square, Color::Black);
6115 control_score += (white_attackers as f32 - black_attackers as f32) * 0.2;
6116 }
6117
6118 for square in extended_center {
6120 let white_attackers = self.count_attackers(board, square, Color::White);
6121 let black_attackers = self.count_attackers(board, square, Color::Black);
6122 control_score += (white_attackers as f32 - black_attackers as f32) * 0.1;
6123 }
6124
6125 if board.side_to_move() == Color::Black {
6126 control_score = -control_score;
6127 }
6128
6129 control_score
6130 }
6131
6132 fn evaluate_pawn_structure_nnue(&self, board: &Board) -> f32 {
6134 let mut pawn_score = 0.0;
6135
6136 for color in [Color::White, Color::Black] {
6138 let multiplier = if color == board.side_to_move() {
6139 1.0
6140 } else {
6141 -1.0
6142 };
6143
6144 let passed_pawns = self.count_passed_pawns(board, color);
6146 pawn_score += passed_pawns as f32 * 0.3 * multiplier;
6147
6148 let isolated_pawns = self.count_isolated_pawns(board, color);
6150 pawn_score -= isolated_pawns as f32 * 0.2 * multiplier;
6151
6152 let doubled_pawns = self.count_doubled_pawns(board, color);
6154 pawn_score -= doubled_pawns as f32 * 0.15 * multiplier;
6155 }
6156
6157 pawn_score
6158 }
6159
6160 fn creates_pin_on_king(
6162 &self,
6163 board: &Board,
6164 piece_square: Square,
6165 enemy_king_square: Square,
6166 piece: chess::Piece,
6167 ) -> bool {
6168 match piece {
6169 chess::Piece::Bishop => {
6170 let rank_diff = (piece_square.get_rank().to_index() as i8
6172 - enemy_king_square.get_rank().to_index() as i8)
6173 .abs();
6174 let file_diff = (piece_square.get_file().to_index() as i8
6175 - enemy_king_square.get_file().to_index() as i8)
6176 .abs();
6177 rank_diff == file_diff && rank_diff > 0
6178 }
6179 chess::Piece::Rook => {
6180 piece_square.get_rank() == enemy_king_square.get_rank()
6182 || piece_square.get_file() == enemy_king_square.get_file()
6183 }
6184 chess::Piece::Queen => {
6185 self.creates_pin_on_king(
6187 board,
6188 piece_square,
6189 enemy_king_square,
6190 chess::Piece::Bishop,
6191 ) || self.creates_pin_on_king(
6192 board,
6193 piece_square,
6194 enemy_king_square,
6195 chess::Piece::Rook,
6196 )
6197 }
6198 _ => false,
6199 }
6200 }
6201
6202 fn knight_can_fork(&self, board: &Board, knight_square: Square, color: Color) -> bool {
6204 let enemy_color = !color;
6205 let mut valuable_targets = 0;
6206
6207 let knight_moves = [
6209 (-2, -1),
6210 (-2, 1),
6211 (-1, -2),
6212 (-1, 2),
6213 (1, -2),
6214 (1, 2),
6215 (2, -1),
6216 (2, 1),
6217 ];
6218
6219 for (rank_offset, file_offset) in knight_moves {
6220 let new_rank = knight_square.get_rank().to_index() as i8 + rank_offset;
6221 let new_file = knight_square.get_file().to_index() as i8 + file_offset;
6222
6223 if new_rank >= 0 && new_rank <= 7 && new_file >= 0 && new_file <= 7 {
6224 let target_square = Square::make_square(
6225 chess::Rank::from_index(new_rank as usize),
6226 chess::File::from_index(new_file as usize),
6227 );
6228
6229 if let Some(piece) = board.piece_on(target_square) {
6230 if board.color_on(target_square) == Some(enemy_color)
6231 && piece != chess::Piece::Pawn
6232 {
6233 valuable_targets += 1;
6234 }
6235 }
6236 }
6237 }
6238
6239 valuable_targets >= 2
6240 }
6241
6242 fn pawn_can_fork(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
6244 let enemy_color = !color;
6245 let direction = if color == Color::White { 1 } else { -1 };
6246 let new_rank = pawn_square.get_rank().to_index() as i8 + direction;
6247
6248 if new_rank < 0 || new_rank > 7 {
6249 return false;
6250 }
6251
6252 let mut fork_targets = 0;
6253
6254 for file_offset in [-1, 1] {
6256 let new_file = pawn_square.get_file().to_index() as i8 + file_offset;
6257 if new_file >= 0 && new_file <= 7 {
6258 let target_square = Square::make_square(
6259 chess::Rank::from_index(new_rank as usize),
6260 chess::File::from_index(new_file as usize),
6261 );
6262
6263 if let Some(piece) = board.piece_on(target_square) {
6264 if board.color_on(target_square) == Some(enemy_color)
6265 && piece != chess::Piece::Pawn
6266 {
6267 fork_targets += 1;
6268 }
6269 }
6270 }
6271 }
6272
6273 fork_targets >= 2
6274 }
6275
6276 fn can_create_discovered_attack(
6278 &self,
6279 board: &Board,
6280 piece_square: Square,
6281 enemy_king_square: Square,
6282 ) -> bool {
6283 let directions = [
6288 (0, 1),
6289 (0, -1),
6290 (1, 0),
6291 (-1, 0), (1, 1),
6293 (1, -1),
6294 (-1, 1),
6295 (-1, -1), ];
6297
6298 for (rank_dir, file_dir) in directions {
6299 if self.has_piece_behind_for_discovered_attack(
6300 board,
6301 piece_square,
6302 enemy_king_square,
6303 rank_dir,
6304 file_dir,
6305 ) {
6306 return true;
6307 }
6308 }
6309
6310 false
6311 }
6312
6313 fn has_piece_behind_for_discovered_attack(
6315 &self,
6316 board: &Board,
6317 piece_square: Square,
6318 enemy_king_square: Square,
6319 rank_dir: i8,
6320 file_dir: i8,
6321 ) -> bool {
6322 let mut current_rank = piece_square.get_rank().to_index() as i8 - rank_dir;
6324 let mut current_file = piece_square.get_file().to_index() as i8 - file_dir;
6325
6326 while current_rank >= 0 && current_rank <= 7 && current_file >= 0 && current_file <= 7 {
6328 let check_square = Square::make_square(
6329 chess::Rank::from_index(current_rank as usize),
6330 chess::File::from_index(current_file as usize),
6331 );
6332
6333 if let Some(piece) = board.piece_on(check_square) {
6334 if board.color_on(check_square) == board.color_on(piece_square) {
6335 if (piece == chess::Piece::Rook || piece == chess::Piece::Queen)
6337 && (rank_dir == 0 || file_dir == 0)
6338 {
6339 return self.has_clear_path(check_square, enemy_king_square, board);
6340 }
6341 if (piece == chess::Piece::Bishop || piece == chess::Piece::Queen)
6342 && (rank_dir.abs() == file_dir.abs())
6343 {
6344 return self.has_clear_path(check_square, enemy_king_square, board);
6345 }
6346 }
6347 break; }
6349
6350 current_rank -= rank_dir;
6351 current_file -= file_dir;
6352 }
6353
6354 false
6355 }
6356
6357 fn count_passed_pawns(&self, board: &Board, color: Color) -> u8 {
6359 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
6361 let mut passed_count = 0;
6362
6363 for pawn_square in pawns {
6364 if self.is_passed_pawn_nnue(board, pawn_square, color) {
6365 passed_count += 1;
6366 }
6367 }
6368
6369 passed_count
6370 }
6371
6372 fn count_isolated_pawns(&self, board: &Board, color: Color) -> u8 {
6373 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
6374 let mut isolated_count = 0;
6375
6376 for pawn_square in pawns {
6377 if self.is_isolated_pawn(board, pawn_square, color) {
6378 isolated_count += 1;
6379 }
6380 }
6381
6382 isolated_count
6383 }
6384
6385 fn count_doubled_pawns(&self, board: &Board, color: Color) -> u8 {
6386 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
6387 let mut file_counts = [0u8; 8];
6388
6389 for pawn_square in pawns {
6390 file_counts[pawn_square.get_file().to_index()] += 1;
6391 }
6392
6393 file_counts
6394 .iter()
6395 .map(|&count| if count > 1 { count - 1 } else { 0 })
6396 .sum()
6397 }
6398
6399 fn is_passed_pawn_nnue(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
6401 let enemy_color = !color;
6402 let pawn_file = pawn_square.get_file();
6403 let direction = if color == Color::White { 1 } else { -1 };
6404
6405 let mut rank = pawn_square.get_rank().to_index() as i8 + direction;
6407
6408 while rank >= 0 && rank <= 7 {
6409 for file_offset in -1..=1 {
6410 let check_file = pawn_file.to_index() as i8 + file_offset;
6411 if check_file >= 0 && check_file <= 7 {
6412 let check_square = Square::make_square(
6413 chess::Rank::from_index(rank as usize),
6414 chess::File::from_index(check_file as usize),
6415 );
6416
6417 if let Some(piece) = board.piece_on(check_square) {
6418 if piece == chess::Piece::Pawn
6419 && board.color_on(check_square) == Some(enemy_color)
6420 {
6421 return false; }
6423 }
6424 }
6425 }
6426 rank += direction;
6427 }
6428
6429 true
6430 }
6431
6432 fn is_isolated_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
6434 let pawn_file = pawn_square.get_file().to_index();
6435 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
6436
6437 for file_offset in [-1, 1] {
6439 let check_file = pawn_file as i8 + file_offset;
6440 if check_file >= 0 && check_file <= 7 {
6441 for pawn_check_square in pawns {
6442 if pawn_check_square.get_file().to_index() == check_file as usize {
6443 return false; }
6445 }
6446 }
6447 }
6448
6449 true }
6451
6452 fn evaluate_space_advantage(&self, board: &Board) -> f32 {
6454 let mut space_score = 0.0;
6455 let moving_color = board.side_to_move();
6456
6457 let white_space = self.count_controlled_squares(board, Color::White);
6459 let black_space = self.count_controlled_squares(board, Color::Black);
6460
6461 let space_difference = white_space as f32 - black_space as f32;
6462
6463 if moving_color == Color::White {
6464 space_score = space_difference * 0.02; } else {
6466 space_score = -space_difference * 0.02;
6467 }
6468
6469 for color in [Color::White, Color::Black] {
6471 let multiplier = if color == moving_color { 1.0 } else { -1.0 };
6472 let advanced_pawns = self.count_advanced_pawns(board, color);
6473 space_score += advanced_pawns as f32 * 0.05 * multiplier;
6474 }
6475
6476 space_score
6477 }
6478
6479 fn evaluate_piece_coordination(&self, board: &Board) -> f32 {
6481 let mut coordination_score = 0.0;
6482 let moving_color = board.side_to_move();
6483
6484 for color in [Color::White, Color::Black] {
6486 let multiplier = if color == moving_color { 1.0 } else { -1.0 };
6487
6488 let rook_coordination = self.evaluate_rook_coordination(board, color);
6490 coordination_score += rook_coordination * 0.1 * multiplier;
6491
6492 if self.has_bishop_pair_coordination(board, color) {
6494 coordination_score += 0.15 * multiplier;
6495 }
6496
6497 let support_chains = self.count_piece_support_chains(board, color);
6499 coordination_score += support_chains as f32 * 0.05 * multiplier;
6500
6501 let knight_coordination = self.evaluate_knight_coordination(board, color);
6503 coordination_score += knight_coordination * 0.08 * multiplier;
6504 }
6505
6506 coordination_score
6507 }
6508
6509 fn evaluate_dynamic_potential(&self, board: &Board) -> f32 {
6511 let mut dynamic_score = 0.0;
6512 let moving_color = board.side_to_move();
6513
6514 let forcing_moves = self.count_forcing_moves(board, moving_color);
6516 dynamic_score += forcing_moves as f32 * 0.04;
6517
6518 let enemy_color = !moving_color;
6520 let enemy_king_square = board.king_square(enemy_color);
6521 let king_attackers = self.count_attackers(board, enemy_king_square, moving_color);
6522 dynamic_score += king_attackers as f32 * 0.06;
6523
6524 let piece_activity = self.evaluate_total_piece_activity(board, moving_color);
6526 dynamic_score += piece_activity / 1000.0; let pawn_breaks = self.count_potential_pawn_breaks(board, moving_color);
6530 dynamic_score += pawn_breaks as f32 * 0.07;
6531
6532 if self.is_opening_phase(board) {
6534 let dev_advantage = self.count_developed_pieces(board, moving_color) as i8
6535 - self.count_developed_pieces(board, enemy_color) as i8;
6536 if dev_advantage > 0 {
6537 dynamic_score += dev_advantage as f32 * 0.03;
6538 }
6539 }
6540
6541 dynamic_score
6542 }
6543
6544 fn evaluate_long_term_advantages(&self, board: &Board) -> f32 {
6546 let mut long_term_score = 0.0;
6547 let moving_color = board.side_to_move();
6548
6549 for color in [Color::White, Color::Black] {
6551 let multiplier = if color == moving_color { 1.0 } else { -1.0 };
6552
6553 let weak_squares = self.count_weak_squares_in_enemy_camp(board, color);
6555 long_term_score += weak_squares as f32 * 0.04 * multiplier;
6556
6557 let outposts = self.count_piece_outposts(board, color);
6559 long_term_score += outposts as f32 * 0.06 * multiplier;
6560
6561 let structure_advantage = self.evaluate_pawn_structure_advantage(board, color);
6563 long_term_score += structure_advantage * 0.05 * multiplier;
6564
6565 let file_control = self.evaluate_file_control(board, color);
6567 long_term_score += file_control * 0.03 * multiplier;
6568 }
6569
6570 long_term_score
6571 }
6572
6573 fn count_controlled_squares(&self, board: &Board, color: Color) -> u8 {
6575 let mut controlled = 0;
6576
6577 let important_squares = if color == Color::White {
6579 [
6581 (3, 3),
6582 (3, 4),
6583 (4, 3),
6584 (4, 4), (5, 0),
6586 (5, 1),
6587 (5, 2),
6588 (5, 3),
6589 (5, 4),
6590 (5, 5),
6591 (5, 6),
6592 (5, 7), (6, 0),
6594 (6, 1),
6595 (6, 2),
6596 (6, 3),
6597 (6, 4),
6598 (6, 5),
6599 (6, 6),
6600 (6, 7), ]
6602 } else {
6603 [
6605 (3, 3),
6606 (3, 4),
6607 (4, 3),
6608 (4, 4), (2, 0),
6610 (2, 1),
6611 (2, 2),
6612 (2, 3),
6613 (2, 4),
6614 (2, 5),
6615 (2, 6),
6616 (2, 7), (1, 0),
6618 (1, 1),
6619 (1, 2),
6620 (1, 3),
6621 (1, 4),
6622 (1, 5),
6623 (1, 6),
6624 (1, 7), ]
6626 };
6627
6628 for (rank, file) in important_squares {
6629 let square =
6630 Square::make_square(chess::Rank::from_index(rank), chess::File::from_index(file));
6631
6632 if self.count_attackers(board, square, color) > 0 {
6633 controlled += 1;
6634 }
6635 }
6636
6637 controlled
6638 }
6639
6640 fn count_advanced_pawns(&self, board: &Board, color: Color) -> u8 {
6642 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
6643 let mut advanced_count = 0;
6644
6645 for pawn_square in pawns {
6646 let rank = pawn_square.get_rank().to_index();
6647 let is_advanced = if color == Color::White {
6648 rank >= 4 } else {
6650 rank <= 3 };
6652
6653 if is_advanced {
6654 advanced_count += 1;
6655 }
6656 }
6657
6658 advanced_count
6659 }
6660
6661 fn evaluate_rook_coordination(&self, board: &Board, color: Color) -> f32 {
6663 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
6664 let mut coordination = 0.0;
6665
6666 let rook_squares: Vec<Square> = rooks.collect();
6667
6668 for i in 0..rook_squares.len() {
6670 for j in (i + 1)..rook_squares.len() {
6671 let rook1 = rook_squares[i];
6672 let rook2 = rook_squares[j];
6673
6674 if rook1.get_file() == rook2.get_file() || rook1.get_rank() == rook2.get_rank() {
6676 coordination += 1.0;
6677 }
6678 }
6679 }
6680
6681 coordination
6682 }
6683
6684 fn has_bishop_pair_coordination(&self, board: &Board, color: Color) -> bool {
6686 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
6687 bishops.popcnt() >= 2
6688 }
6689
6690 fn count_piece_support_chains(&self, board: &Board, color: Color) -> u8 {
6692 let mut support_count = 0;
6693 let pieces = board.color_combined(color);
6694
6695 for piece_square in *pieces {
6696 if let Some(piece) = board.piece_on(piece_square) {
6697 if piece != chess::Piece::King {
6698 let defenders = self.count_attackers(board, piece_square, color);
6700 if defenders > 0 {
6701 support_count += 1;
6702 }
6703 }
6704 }
6705 }
6706
6707 support_count
6708 }
6709
6710 fn evaluate_knight_coordination(&self, board: &Board, color: Color) -> f32 {
6712 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
6713 let mut coordination = 0.0;
6714
6715 let knight_squares: Vec<Square> = knights.collect();
6716
6717 for i in 0..knight_squares.len() {
6719 for j in (i + 1)..knight_squares.len() {
6720 let knight1 = knight_squares[i];
6721 let knight2 = knight_squares[j];
6722
6723 let rank_diff = (knight1.get_rank().to_index() as i8
6725 - knight2.get_rank().to_index() as i8)
6726 .abs();
6727 let file_diff = (knight1.get_file().to_index() as i8
6728 - knight2.get_file().to_index() as i8)
6729 .abs();
6730
6731 if rank_diff <= 3 && file_diff <= 3 {
6732 coordination += 0.5;
6733 }
6734 }
6735 }
6736
6737 coordination
6738 }
6739
6740 fn count_forcing_moves(&self, board: &Board, color: Color) -> u8 {
6742 let mut forcing_count = 0;
6743 let moves = MoveGen::new_legal(board);
6744
6745 for chess_move in moves {
6746 if self.is_forcing_move(&chess_move, board) {
6747 forcing_count += 1;
6748 }
6749 }
6750
6751 forcing_count
6752 }
6753
6754 fn is_forcing_move(&self, chess_move: &ChessMove, board: &Board) -> bool {
6756 if board.piece_on(chess_move.get_dest()).is_some() {
6758 return true;
6759 }
6760
6761 let mut temp_board = *board;
6763 temp_board = temp_board.make_move_new(*chess_move);
6764 if temp_board.checkers().popcnt() > 0 {
6765 return true;
6766 }
6767
6768 let threatens_valuable = self.threatens_valuable_piece(chess_move, board);
6770 if threatens_valuable {
6771 return true;
6772 }
6773
6774 false
6775 }
6776
6777 fn threatens_valuable_piece(&self, chess_move: &ChessMove, board: &Board) -> bool {
6779 let mut temp_board = *board;
6780 temp_board = temp_board.make_move_new(*chess_move);
6781
6782 let moving_color = board.side_to_move();
6783 let enemy_color = !moving_color;
6784 let enemy_pieces = board.color_combined(enemy_color);
6785
6786 for enemy_square in *enemy_pieces {
6787 if let Some(piece) = temp_board.piece_on(enemy_square) {
6788 match piece {
6789 chess::Piece::Queen
6790 | chess::Piece::Rook
6791 | chess::Piece::Bishop
6792 | chess::Piece::Knight => {
6793 if self.count_attackers(&temp_board, enemy_square, moving_color) > 0 {
6794 return true;
6795 }
6796 }
6797 _ => {}
6798 }
6799 }
6800 }
6801
6802 false
6803 }
6804
6805 fn evaluate_total_piece_activity(&self, board: &Board, color: Color) -> f32 {
6807 let mut total_activity = 0.0;
6808 let pieces = board.color_combined(color);
6809
6810 for piece_square in *pieces {
6811 if let Some(piece) = board.piece_on(piece_square) {
6812 let mobility = self.calculate_piece_mobility_at_square(board, piece_square, piece);
6813 total_activity += mobility;
6814 }
6815 }
6816
6817 total_activity
6818 }
6819
6820 fn calculate_piece_mobility_at_square(
6822 &self,
6823 board: &Board,
6824 piece_square: Square,
6825 piece: chess::Piece,
6826 ) -> f32 {
6827 let mut mobility = 0.0;
6828
6829 match piece {
6830 chess::Piece::Queen => {
6831 for direction in [
6833 (0, 1),
6834 (0, -1),
6835 (1, 0),
6836 (-1, 0),
6837 (1, 1),
6838 (1, -1),
6839 (-1, 1),
6840 (-1, -1),
6841 ] {
6842 mobility +=
6843 self.count_moves_in_direction(board, piece_square, direction) as f32 * 1.5;
6844 }
6845 }
6846 chess::Piece::Rook => {
6847 for direction in [(0, 1), (0, -1), (1, 0), (-1, 0)] {
6849 mobility +=
6850 self.count_moves_in_direction(board, piece_square, direction) as f32 * 1.2;
6851 }
6852 }
6853 chess::Piece::Bishop => {
6854 for direction in [(1, 1), (1, -1), (-1, 1), (-1, -1)] {
6856 mobility +=
6857 self.count_moves_in_direction(board, piece_square, direction) as f32;
6858 }
6859 }
6860 chess::Piece::Knight => {
6861 let knight_moves = [
6863 (-2, -1),
6864 (-2, 1),
6865 (-1, -2),
6866 (-1, 2),
6867 (1, -2),
6868 (1, 2),
6869 (2, -1),
6870 (2, 1),
6871 ];
6872
6873 for (rank_offset, file_offset) in knight_moves {
6874 let new_rank = piece_square.get_rank().to_index() as i8 + rank_offset;
6875 let new_file = piece_square.get_file().to_index() as i8 + file_offset;
6876
6877 if new_rank >= 0 && new_rank <= 7 && new_file >= 0 && new_file <= 7 {
6878 let target_square = Square::make_square(
6879 chess::Rank::from_index(new_rank as usize),
6880 chess::File::from_index(new_file as usize),
6881 );
6882
6883 if board.piece_on(target_square).is_none()
6884 || board.color_on(target_square) != board.color_on(piece_square)
6885 {
6886 mobility += 1.0;
6887 }
6888 }
6889 }
6890 }
6891 _ => {}
6892 }
6893
6894 mobility
6895 }
6896
6897 fn count_moves_in_direction(
6899 &self,
6900 board: &Board,
6901 start_square: Square,
6902 direction: (i8, i8),
6903 ) -> u8 {
6904 let mut move_count = 0;
6905 let (rank_dir, file_dir) = direction;
6906 let piece_color = board.color_on(start_square);
6907
6908 let mut current_rank = start_square.get_rank().to_index() as i8 + rank_dir;
6909 let mut current_file = start_square.get_file().to_index() as i8 + file_dir;
6910
6911 while current_rank >= 0 && current_rank <= 7 && current_file >= 0 && current_file <= 7 {
6912 let target_square = Square::make_square(
6913 chess::Rank::from_index(current_rank as usize),
6914 chess::File::from_index(current_file as usize),
6915 );
6916
6917 if let Some(target_piece) = board.piece_on(target_square) {
6918 if board.color_on(target_square) != piece_color {
6919 move_count += 1; }
6921 break; } else {
6923 move_count += 1; }
6925
6926 current_rank += rank_dir;
6927 current_file += file_dir;
6928 }
6929
6930 move_count
6931 }
6932
6933 fn count_potential_pawn_breaks(&self, board: &Board, color: Color) -> u8 {
6935 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
6936 let mut break_count = 0;
6937
6938 for pawn_square in pawns {
6939 if self.can_create_pawn_break(board, pawn_square, color) {
6940 break_count += 1;
6941 }
6942 }
6943
6944 break_count
6945 }
6946
6947 fn can_create_pawn_break(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
6949 let direction = if color == Color::White { 1 } else { -1 };
6950 let current_rank = pawn_square.get_rank().to_index() as i8;
6951 let next_rank = current_rank + direction;
6952
6953 if next_rank < 0 || next_rank > 7 {
6954 return false;
6955 }
6956
6957 let file = pawn_square.get_file();
6959 let target_square = Square::make_square(chess::Rank::from_index(next_rank as usize), file);
6960
6961 if board.piece_on(target_square).is_none() {
6963 for file_offset in [-1, 1] {
6965 let attack_file = file.to_index() as i8 + file_offset;
6966 if attack_file >= 0 && attack_file <= 7 {
6967 let attack_square = Square::make_square(
6968 chess::Rank::from_index(next_rank as usize),
6969 chess::File::from_index(attack_file as usize),
6970 );
6971
6972 if let Some(piece) = board.piece_on(attack_square) {
6973 if piece == chess::Piece::Pawn
6974 && board.color_on(attack_square) == Some(!color)
6975 {
6976 return true; }
6978 }
6979 }
6980 }
6981 }
6982
6983 false
6984 }
6985
6986 fn count_weak_squares_in_enemy_camp(&self, board: &Board, color: Color) -> u8 {
6988 let mut weak_squares = 0;
6989 let enemy_color = !color;
6990
6991 let enemy_ranks = if color == Color::White {
6993 vec![5, 6, 7] } else {
6995 vec![2, 1, 0] };
6997
6998 for rank in enemy_ranks {
6999 for file in 0..8 {
7000 let square = Square::make_square(
7001 chess::Rank::from_index(rank),
7002 chess::File::from_index(file),
7003 );
7004
7005 if self.is_weak_square(board, square, enemy_color) {
7006 weak_squares += 1;
7007 }
7008 }
7009 }
7010
7011 weak_squares
7012 }
7013
7014 fn is_weak_square(&self, board: &Board, square: Square, color: Color) -> bool {
7016 let pawn_defenders = self.count_pawn_defenders(board, square, color);
7022 if pawn_defenders > 0 {
7023 return false; }
7025
7026 let enemy_attackers = self.count_attackers(board, square, !color);
7028 let friendly_defenders = self.count_attackers(board, square, color);
7029
7030 enemy_attackers > friendly_defenders
7031 }
7032
7033 fn count_pawn_defenders(&self, board: &Board, square: Square, color: Color) -> u8 {
7035 let mut defenders = 0;
7036 let square_rank = square.get_rank().to_index() as i8;
7037 let square_file = square.get_file().to_index() as i8;
7038
7039 let pawn_rank_offset = if color == Color::White { -1 } else { 1 };
7041 let pawn_rank = square_rank + pawn_rank_offset;
7042
7043 if pawn_rank >= 0 && pawn_rank <= 7 {
7044 for file_offset in [-1, 1] {
7045 let pawn_file = square_file + file_offset;
7046 if pawn_file >= 0 && pawn_file <= 7 {
7047 let pawn_square = Square::make_square(
7048 chess::Rank::from_index(pawn_rank as usize),
7049 chess::File::from_index(pawn_file as usize),
7050 );
7051
7052 if let Some(piece) = board.piece_on(pawn_square) {
7053 if piece == chess::Piece::Pawn && board.color_on(pawn_square) == Some(color)
7054 {
7055 defenders += 1;
7056 }
7057 }
7058 }
7059 }
7060 }
7061
7062 defenders
7063 }
7064
7065 fn count_piece_outposts(&self, board: &Board, color: Color) -> u8 {
7067 let mut outposts = 0;
7068 let pieces = board.color_combined(color);
7069
7070 for piece_square in *pieces {
7071 if let Some(piece) = board.piece_on(piece_square) {
7072 match piece {
7073 chess::Piece::Knight | chess::Piece::Bishop => {
7074 if self.is_outpost(board, piece_square, color) {
7075 outposts += 1;
7076 }
7077 }
7078 _ => {}
7079 }
7080 }
7081 }
7082
7083 outposts
7084 }
7085
7086 fn is_outpost(&self, board: &Board, piece_square: Square, color: Color) -> bool {
7088 let rank = piece_square.get_rank().to_index();
7094 let is_advanced = if color == Color::White {
7095 rank >= 4 } else {
7097 rank <= 3 };
7099
7100 if !is_advanced {
7101 return false;
7102 }
7103
7104 let pawn_support = self.count_pawn_defenders(board, piece_square, color) > 0;
7106
7107 let enemy_pawn_attacks = self.count_pawn_defenders(board, piece_square, !color) == 0;
7109
7110 pawn_support && enemy_pawn_attacks
7111 }
7112
7113 fn evaluate_pawn_structure_advantage(&self, board: &Board, color: Color) -> f32 {
7115 let mut advantage = 0.0;
7116
7117 let passed_pawns = self.count_passed_pawns(board, color);
7119 advantage += passed_pawns as f32 * 0.3;
7120
7121 let pawn_chains = self.count_pawn_chains(board, color);
7123 advantage += pawn_chains as f32 * 0.1;
7124
7125 let connected_passed = self.count_connected_passed_pawns(board, color);
7127 advantage += connected_passed as f32 * 0.5;
7128
7129 advantage
7130 }
7131
7132 fn count_pawn_chains(&self, board: &Board, color: Color) -> u8 {
7134 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
7135 let mut chains = 0;
7136
7137 for pawn_square in pawns {
7138 if self.is_part_of_pawn_chain(board, pawn_square, color) {
7139 chains += 1;
7140 }
7141 }
7142
7143 chains / 2 }
7145
7146 fn is_part_of_pawn_chain(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
7148 let pawn_file = pawn_square.get_file().to_index() as i8;
7149 let pawn_rank = pawn_square.get_rank().to_index() as i8;
7150
7151 let diagonal_offsets = if color == Color::White {
7153 [(-1, -1), (-1, 1)] } else {
7155 [(1, -1), (1, 1)] };
7157
7158 for (rank_offset, file_offset) in diagonal_offsets {
7159 let check_rank = pawn_rank + rank_offset;
7160 let check_file = pawn_file + file_offset;
7161
7162 if check_rank >= 0 && check_rank <= 7 && check_file >= 0 && check_file <= 7 {
7163 let check_square = Square::make_square(
7164 chess::Rank::from_index(check_rank as usize),
7165 chess::File::from_index(check_file as usize),
7166 );
7167
7168 if let Some(piece) = board.piece_on(check_square) {
7169 if piece == chess::Piece::Pawn && board.color_on(check_square) == Some(color) {
7170 return true;
7171 }
7172 }
7173 }
7174 }
7175
7176 false
7177 }
7178
7179 fn count_connected_passed_pawns(&self, board: &Board, color: Color) -> u8 {
7181 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
7182 let mut connected_passed = 0;
7183
7184 for pawn_square in pawns {
7185 if self.is_passed_pawn_nnue(board, pawn_square, color)
7186 && self.has_connected_passed_pawn(board, pawn_square, color)
7187 {
7188 connected_passed += 1;
7189 }
7190 }
7191
7192 connected_passed / 2 }
7194
7195 fn has_connected_passed_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
7197 let pawn_file = pawn_square.get_file().to_index() as i8;
7198
7199 for file_offset in [-1, 1] {
7201 let check_file = pawn_file + file_offset;
7202 if check_file >= 0 && check_file <= 7 {
7203 let file_pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
7204 for other_pawn in file_pawns {
7205 if other_pawn.get_file().to_index() == check_file as usize
7206 && self.is_passed_pawn_nnue(board, other_pawn, color)
7207 {
7208 return true;
7209 }
7210 }
7211 }
7212 }
7213
7214 false
7215 }
7216
7217 fn evaluate_file_control(&self, board: &Board, color: Color) -> f32 {
7219 let mut file_control = 0.0;
7220
7221 for file_index in 0..8 {
7223 let file = chess::File::from_index(file_index);
7224 let file_type = self.classify_file_type(board, file, color);
7225
7226 match file_type {
7227 FileType::Open => {
7228 let control = self.evaluate_open_file_control(board, file, color);
7229 file_control += control * 0.2;
7230 }
7231 FileType::SemiOpen => {
7232 let control = self.evaluate_semi_open_file_control(board, file, color);
7233 file_control += control * 0.15;
7234 }
7235 _ => {}
7236 }
7237 }
7238
7239 file_control
7240 }
7241
7242 fn classify_file_type(&self, board: &Board, file: chess::File, color: Color) -> FileType {
7244 let own_pawns_on_file = self.count_pawns_on_file(board, file, color);
7245 let enemy_pawns_on_file = self.count_pawns_on_file(board, file, !color);
7246
7247 match (own_pawns_on_file, enemy_pawns_on_file) {
7248 (0, 0) => FileType::Open,
7249 (0, _) => FileType::SemiOpen,
7250 (_, 0) => FileType::SemiOpen,
7251 _ => FileType::Closed,
7252 }
7253 }
7254
7255 fn count_pawns_on_file(&self, board: &Board, file: chess::File, color: Color) -> u8 {
7257 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
7258 let mut count = 0;
7259
7260 for pawn_square in pawns {
7261 if pawn_square.get_file() == file {
7262 count += 1;
7263 }
7264 }
7265
7266 count
7267 }
7268
7269 fn evaluate_open_file_control(&self, board: &Board, file: chess::File, color: Color) -> f32 {
7271 let mut control = 0.0;
7272
7273 for rank_index in 0..8 {
7275 let square = Square::make_square(chess::Rank::from_index(rank_index), file);
7276
7277 if let Some(piece) = board.piece_on(square) {
7278 if board.color_on(square) == Some(color) {
7279 match piece {
7280 chess::Piece::Rook => control += 1.0,
7281 chess::Piece::Queen => control += 1.5,
7282 _ => {}
7283 }
7284 }
7285 }
7286 }
7287
7288 control
7289 }
7290
7291 fn evaluate_semi_open_file_control(
7293 &self,
7294 board: &Board,
7295 file: chess::File,
7296 color: Color,
7297 ) -> f32 {
7298 self.evaluate_open_file_control(board, file, color) * 0.7
7300 }
7301
7302 fn get_pattern_evaluation(&self, board: &Board) -> (f32, f32) {
7304 let confidence = if self.is_opening_phase(board) {
7308 0.7 } else if self.is_endgame_phase(board) {
7310 0.8 } else {
7312 0.5 };
7314
7315 let tactical_bonus = self.tactical_bonuses(board);
7317 let eval = (tactical_bonus / 100.0).clamp(-2.0, 2.0);
7318
7319 (eval, confidence)
7320 }
7321
7322 fn get_strategic_initiative_evaluation(&self, board: &Board) -> f32 {
7324 let strategic_eval = self.strategic_evaluator.evaluate_strategic(board);
7326 let strategic_score_cp = strategic_eval.total_evaluation;
7327
7328 let mut initiative = strategic_score_cp / 100.0;
7330
7331 let space_advantage = self.evaluate_space_advantage(board);
7333 let piece_coordination = self.evaluate_piece_coordination(board);
7334 let dynamic_potential = self.evaluate_dynamic_potential(board);
7335 let long_term_advantages = self.evaluate_long_term_advantages(board);
7336
7337 initiative += space_advantage * 0.15; initiative += piece_coordination * 0.20; initiative += dynamic_potential * 0.25; initiative += long_term_advantages * 0.10; let white_dev = self.count_developed_pieces(board, Color::White);
7345 let black_dev = self.count_developed_pieces(board, Color::Black);
7346 initiative += (white_dev as f32 - black_dev as f32) * 0.08;
7347
7348 let center_control = self.evaluate_center_control(board);
7350 initiative += (center_control / 100.0) * 0.1; let white_safety = self.evaluate_king_safety_for_color(board, Color::White);
7354 let black_safety = self.evaluate_king_safety_for_color(board, Color::Black);
7355 initiative += ((white_safety - black_safety) / 100.0) * 0.1; initiative.clamp(-1.5, 1.5)
7359 }
7360
7361 fn blend_evaluations(
7363 &self,
7364 nnue_eval: f32,
7365 pattern_eval: f32,
7366 nnue_confidence: f32,
7367 pattern_confidence: f32,
7368 ) -> f32 {
7369 let total_confidence = nnue_confidence + pattern_confidence;
7370
7371 if total_confidence > 0.0 {
7372 (nnue_eval * nnue_confidence + pattern_eval * pattern_confidence) / total_confidence
7373 } else {
7374 (nnue_eval + pattern_eval) / 2.0
7376 }
7377 }
7378
7379 fn count_material(&self, board: &Board) -> u32 {
7381 let mut count = 0;
7382 for square in chess::ALL_SQUARES {
7383 if board.piece_on(square).is_some() {
7384 count += 1;
7385 }
7386 }
7387 count
7388 }
7389
7390 fn has_tactical_patterns(&self, board: &Board) -> bool {
7392 board.checkers().popcnt() > 0 || self.has_hanging_pieces(board) || self.has_pins_or_forks(board) }
7397
7398 fn assess_tactical_threat_level(&self, board: &Board) -> u8 {
7400 let mut threat_level = 0;
7401
7402 if board.checkers().popcnt() > 0 {
7404 threat_level += 1;
7405 }
7406
7407 if self.has_hanging_pieces(board) {
7409 threat_level += 1;
7410 }
7411
7412 if self.has_pins_or_forks(board) {
7414 threat_level += 1;
7415 }
7416
7417 if self.has_checkmate_threats(board) {
7419 threat_level += 2;
7420 }
7421
7422 if self.has_king_safety_threats(board) {
7424 threat_level += 1;
7425 }
7426
7427 threat_level.min(3) }
7429
7430 fn has_checkmate_threats(&self, board: &Board) -> bool {
7432 for color in [Color::White, Color::Black] {
7434 let king_square = board.king_square(color);
7435 let back_rank = if color == Color::White {
7436 chess::Rank::First
7437 } else {
7438 chess::Rank::Eighth
7439 };
7440
7441 if king_square.get_rank() == back_rank {
7443 let escape_squares = self.count_king_escape_squares(board, king_square);
7444 if escape_squares <= 1 {
7445 return true;
7446 }
7447 }
7448 }
7449
7450 false
7451 }
7452
7453 fn has_king_safety_threats(&self, board: &Board) -> bool {
7455 for color in [Color::White, Color::Black] {
7456 let king_square = board.king_square(color);
7457 let enemy_color = !color;
7458
7459 let king_zone_attacks = self.count_king_zone_attacks(board, king_square, enemy_color);
7461 if king_zone_attacks >= 2 {
7462 return true;
7463 }
7464 }
7465
7466 false
7467 }
7468
7469 fn count_king_escape_squares(&self, board: &Board, king_square: Square) -> u8 {
7471 let mut escape_count = 0;
7472 let king_color = board.color_on(king_square).unwrap();
7473
7474 for rank_offset in -1..=1 {
7476 for file_offset in -1..=1 {
7477 if rank_offset == 0 && file_offset == 0 {
7478 continue;
7479 }
7480
7481 let new_rank = king_square.get_rank().to_index() as i8 + rank_offset;
7482 let new_file = king_square.get_file().to_index() as i8 + file_offset;
7483
7484 if new_rank >= 0 && new_rank <= 7 && new_file >= 0 && new_file <= 7 {
7485 let escape_square = Square::make_square(
7486 chess::Rank::from_index(new_rank as usize),
7487 chess::File::from_index(new_file as usize),
7488 );
7489
7490 if board.piece_on(escape_square).is_none()
7492 || board.color_on(escape_square) != Some(king_color)
7493 {
7494 if self.count_attackers(board, escape_square, !king_color) == 0 {
7496 escape_count += 1;
7497 }
7498 }
7499 }
7500 }
7501 }
7502
7503 escape_count
7504 }
7505
7506 fn count_king_zone_attacks(
7508 &self,
7509 board: &Board,
7510 king_square: Square,
7511 attacking_color: Color,
7512 ) -> u8 {
7513 let mut attack_count = 0;
7514
7515 for rank_offset in -1..=1 {
7517 for file_offset in -1..=1 {
7518 let new_rank = king_square.get_rank().to_index() as i8 + rank_offset;
7519 let new_file = king_square.get_file().to_index() as i8 + file_offset;
7520
7521 if new_rank >= 0 && new_rank <= 7 && new_file >= 0 && new_file <= 7 {
7522 let zone_square = Square::make_square(
7523 chess::Rank::from_index(new_rank as usize),
7524 chess::File::from_index(new_file as usize),
7525 );
7526
7527 if self.count_attackers(board, zone_square, attacking_color) > 0 {
7528 attack_count += 1;
7529 }
7530 }
7531 }
7532 }
7533
7534 attack_count
7535 }
7536
7537 fn evaluate_tactical_move_bonus(&self, chess_move: &ChessMove, board: &Board) -> i32 {
7539 let mut bonus = 0;
7540
7541 let mut temp_board = *board;
7543 temp_board = temp_board.make_move_new(*chess_move);
7544
7545 let moving_color = board.side_to_move();
7546 let opponent_color = !moving_color;
7547
7548 if self.creates_pin(chess_move, board, &temp_board) {
7550 bonus += 20000; }
7552
7553 if self.creates_fork(chess_move, board, &temp_board) {
7555 bonus += 25000; }
7557
7558 if self.creates_discovered_attack(chess_move, board, &temp_board) {
7560 bonus += 15000; }
7562
7563 let enemy_king_square = temp_board.king_square(opponent_color);
7565 if self.attacks_king_zone(chess_move, &temp_board, enemy_king_square) {
7566 bonus += 10000; }
7568
7569 if let Some(piece) = board.piece_on(chess_move.get_source()) {
7571 if piece == chess::Piece::Knight || piece == chess::Piece::Bishop {
7572 bonus += self.evaluate_centralization_bonus(chess_move, piece);
7573 }
7574 }
7575
7576 bonus
7577 }
7578
7579 fn creates_pin(&self, chess_move: &ChessMove, _board: &Board, temp_board: &Board) -> bool {
7581 let moving_color = temp_board.side_to_move();
7583 let opponent_color = !moving_color;
7584 let enemy_king_square = temp_board.king_square(opponent_color);
7585 let dest_square = chess_move.get_dest();
7586
7587 let rank_diff = (dest_square.get_rank().to_index() as i8
7589 - enemy_king_square.get_rank().to_index() as i8)
7590 .abs();
7591 let file_diff = (dest_square.get_file().to_index() as i8
7592 - enemy_king_square.get_file().to_index() as i8)
7593 .abs();
7594
7595 rank_diff == 0 || file_diff == 0 || rank_diff == file_diff
7597 }
7598
7599 fn creates_fork(&self, chess_move: &ChessMove, _board: &Board, temp_board: &Board) -> bool {
7601 let dest_square = chess_move.get_dest();
7603 let opponent_color = !temp_board.side_to_move();
7604 let mut valuable_targets = 0;
7605
7606 for square in chess::ALL_SQUARES {
7608 if let Some(piece) = temp_board.piece_on(square) {
7609 if temp_board.color_on(square) == Some(opponent_color) {
7610 if piece != chess::Piece::Pawn
7611 && self.can_piece_attack_square(dest_square, square, temp_board)
7612 {
7613 valuable_targets += 1;
7614 }
7615 }
7616 }
7617 }
7618
7619 valuable_targets >= 2
7620 }
7621
7622 fn creates_discovered_attack(
7624 &self,
7625 chess_move: &ChessMove,
7626 board: &Board,
7627 temp_board: &Board,
7628 ) -> bool {
7629 let source_square = chess_move.get_source();
7631 let moving_color = board.side_to_move();
7632 let opponent_color = !moving_color;
7633 let enemy_king_square = temp_board.king_square(opponent_color);
7634
7635 let directions = [
7637 (0, 1),
7638 (0, -1),
7639 (1, 0),
7640 (-1, 0), (1, 1),
7642 (1, -1),
7643 (-1, 1),
7644 (-1, -1), ];
7646
7647 for (rank_dir, file_dir) in directions {
7648 let mut check_square = source_square;
7649
7650 loop {
7652 let new_rank = check_square.get_rank().to_index() as i8 - rank_dir;
7653 let new_file = check_square.get_file().to_index() as i8 - file_dir;
7654
7655 if new_rank < 0 || new_rank > 7 || new_file < 0 || new_file > 7 {
7656 break;
7657 }
7658
7659 check_square = Square::make_square(
7660 chess::Rank::from_index(new_rank as usize),
7661 chess::File::from_index(new_file as usize),
7662 );
7663
7664 if let Some(piece) = board.piece_on(check_square) {
7665 if board.color_on(check_square) == Some(moving_color) {
7666 if (piece == chess::Piece::Rook || piece == chess::Piece::Queen)
7668 && (rank_dir == 0 || file_dir == 0)
7669 {
7670 return self.has_clear_path(
7671 check_square,
7672 enemy_king_square,
7673 temp_board,
7674 );
7675 }
7676 if (piece == chess::Piece::Bishop || piece == chess::Piece::Queen)
7677 && (rank_dir.abs() == file_dir.abs())
7678 {
7679 return self.has_clear_path(
7680 check_square,
7681 enemy_king_square,
7682 temp_board,
7683 );
7684 }
7685 }
7686 break; }
7688 }
7689 }
7690
7691 false
7692 }
7693
7694 fn attacks_king_zone(
7696 &self,
7697 chess_move: &ChessMove,
7698 temp_board: &Board,
7699 enemy_king_square: Square,
7700 ) -> bool {
7701 let dest_square = chess_move.get_dest();
7702 let king_zone_attack_count =
7703 self.count_king_zone_attacks(temp_board, enemy_king_square, temp_board.side_to_move());
7704 king_zone_attack_count > 0
7705 && self.can_piece_attack_square(dest_square, enemy_king_square, temp_board)
7706 }
7707
7708 fn evaluate_centralization_bonus(&self, chess_move: &ChessMove, piece: chess::Piece) -> i32 {
7710 let dest_square = chess_move.get_dest();
7711 let rank = dest_square.get_rank().to_index();
7712 let file = dest_square.get_file().to_index();
7713
7714 let center_distance = (rank as f32 - 3.5).abs() + (file as f32 - 3.5).abs();
7716 let centralization_bonus = (8.0 - center_distance) * 100.0;
7717
7718 match piece {
7719 chess::Piece::Knight => (centralization_bonus * 1.5) as i32, chess::Piece::Bishop => (centralization_bonus * 1.0) as i32,
7721 _ => 0,
7722 }
7723 }
7724
7725 fn can_piece_attack_square(&self, from: Square, to: Square, board: &Board) -> bool {
7727 if let Some(piece) = board.piece_on(from) {
7728 match piece {
7729 chess::Piece::Pawn => {
7730 let rank_diff =
7732 (to.get_rank().to_index() as i8 - from.get_rank().to_index() as i8).abs();
7733 let file_diff =
7734 (to.get_file().to_index() as i8 - from.get_file().to_index() as i8).abs();
7735 rank_diff == 1 && file_diff == 1
7736 }
7737 chess::Piece::Knight => {
7738 let rank_diff =
7740 (to.get_rank().to_index() as i8 - from.get_rank().to_index() as i8).abs();
7741 let file_diff =
7742 (to.get_file().to_index() as i8 - from.get_file().to_index() as i8).abs();
7743 (rank_diff == 2 && file_diff == 1) || (rank_diff == 1 && file_diff == 2)
7744 }
7745 chess::Piece::Bishop => {
7746 let rank_diff =
7748 (to.get_rank().to_index() as i8 - from.get_rank().to_index() as i8).abs();
7749 let file_diff =
7750 (to.get_file().to_index() as i8 - from.get_file().to_index() as i8).abs();
7751 rank_diff == file_diff && rank_diff > 0
7752 }
7753 chess::Piece::Rook => {
7754 from.get_rank() == to.get_rank() || from.get_file() == to.get_file()
7756 }
7757 chess::Piece::Queen => {
7758 let rank_diff =
7760 (to.get_rank().to_index() as i8 - from.get_rank().to_index() as i8).abs();
7761 let file_diff =
7762 (to.get_file().to_index() as i8 - from.get_file().to_index() as i8).abs();
7763 from.get_rank() == to.get_rank()
7764 || from.get_file() == to.get_file()
7765 || (rank_diff == file_diff && rank_diff > 0)
7766 }
7767 chess::Piece::King => {
7768 let rank_diff =
7770 (to.get_rank().to_index() as i8 - from.get_rank().to_index() as i8).abs();
7771 let file_diff =
7772 (to.get_file().to_index() as i8 - from.get_file().to_index() as i8).abs();
7773 rank_diff <= 1 && file_diff <= 1 && (rank_diff + file_diff) > 0
7774 }
7775 }
7776 } else {
7777 false
7778 }
7779 }
7780
7781 fn has_clear_path(&self, from: Square, to: Square, board: &Board) -> bool {
7783 let rank_diff = to.get_rank().to_index() as i8 - from.get_rank().to_index() as i8;
7784 let file_diff = to.get_file().to_index() as i8 - from.get_file().to_index() as i8;
7785
7786 if rank_diff != 0 && file_diff != 0 && rank_diff.abs() != file_diff.abs() {
7788 return false;
7789 }
7790
7791 let rank_step = if rank_diff == 0 {
7792 0
7793 } else {
7794 rank_diff / rank_diff.abs()
7795 };
7796 let file_step = if file_diff == 0 {
7797 0
7798 } else {
7799 file_diff / file_diff.abs()
7800 };
7801
7802 let mut current_rank = from.get_rank().to_index() as i8 + rank_step;
7803 let mut current_file = from.get_file().to_index() as i8 + file_step;
7804
7805 while current_rank != to.get_rank().to_index() as i8
7806 || current_file != to.get_file().to_index() as i8
7807 {
7808 let check_square = Square::make_square(
7809 chess::Rank::from_index(current_rank as usize),
7810 chess::File::from_index(current_file as usize),
7811 );
7812
7813 if board.piece_on(check_square).is_some() {
7814 return false; }
7816
7817 current_rank += rank_step;
7818 current_file += file_step;
7819 }
7820
7821 true
7822 }
7823
7824 fn is_blunder_move(&self, chess_move: &ChessMove, board: &Board) -> bool {
7826 let moving_piece = board.piece_on(chess_move.get_source());
7827 if moving_piece.is_none() {
7828 return false;
7829 }
7830
7831 let piece = moving_piece.unwrap();
7832 let moving_color = board.side_to_move();
7833
7834 if self.creates_hanging_piece(chess_move, board) {
7836 return true;
7837 }
7838
7839 if self.loses_material_without_compensation(chess_move, board) {
7841 return true;
7842 }
7843
7844 if self.exposes_king_to_danger(chess_move, board) {
7846 return true;
7847 }
7848
7849 if self.is_severe_positional_mistake(chess_move, board) {
7851 return true;
7852 }
7853
7854 if self.nnue_indicates_blunder(chess_move, board) {
7856 return true;
7857 }
7858
7859 false
7860 }
7861
7862 fn creates_hanging_piece(&self, chess_move: &ChessMove, board: &Board) -> bool {
7864 let mut temp_board = *board;
7865 temp_board = temp_board.make_move_new(*chess_move);
7866
7867 let moving_color = board.side_to_move();
7868 let dest_square = chess_move.get_dest();
7869
7870 let attackers = self.count_attackers(&temp_board, dest_square, !moving_color);
7872 let defenders = self.count_attackers(&temp_board, dest_square, moving_color);
7873
7874 if attackers > defenders {
7875 let piece_value = if let Some(piece) = temp_board.piece_on(dest_square) {
7876 self.get_piece_value(piece)
7877 } else {
7878 return false;
7879 };
7880
7881 if piece_value > 100 {
7883 return true;
7884 }
7885 }
7886
7887 false
7888 }
7889
7890 fn loses_material_without_compensation(&self, chess_move: &ChessMove, board: &Board) -> bool {
7892 let current_material = self.material_balance(board);
7893
7894 let mut temp_board = *board;
7895 temp_board = temp_board.make_move_new(*chess_move);
7896
7897 let new_material = self.material_balance(&temp_board);
7898 let material_loss = current_material - new_material;
7899
7900 if material_loss > 200.0 {
7902 let king_safety_improvement = self.king_safety(&temp_board) - self.king_safety(board);
7904 let development_improvement = self.evaluate_development_improvement(board, &temp_board);
7905 let attack_potential = self.evaluate_attack_potential(&temp_board);
7906
7907 let total_compensation =
7908 king_safety_improvement + development_improvement + attack_potential;
7909
7910 if total_compensation < material_loss * 0.6 {
7912 return true;
7913 }
7914 }
7915
7916 false
7917 }
7918
7919 fn exposes_king_to_danger(&self, chess_move: &ChessMove, board: &Board) -> bool {
7921 let moving_color = board.side_to_move();
7922 let mut temp_board = *board;
7923 temp_board = temp_board.make_move_new(*chess_move);
7924
7925 let king_square = temp_board.king_square(moving_color);
7926
7927 if temp_board.checkers().popcnt() > 0 {
7929 let escape_squares = self.count_king_escape_squares(&temp_board, king_square);
7931 if escape_squares <= 1 {
7932 return true; }
7934 }
7935
7936 if self.removes_key_king_defender(chess_move, board) {
7938 return true;
7939 }
7940
7941 false
7942 }
7943
7944 fn is_severe_positional_mistake(&self, chess_move: &ChessMove, board: &Board) -> bool {
7946 let current_strategic_eval = self.strategic_evaluator.evaluate_strategic(board);
7948
7949 let mut temp_board = *board;
7950 temp_board = temp_board.make_move_new(*chess_move);
7951
7952 let new_strategic_eval = self.strategic_evaluator.evaluate_strategic(&temp_board);
7953
7954 let strategic_loss =
7955 current_strategic_eval.total_evaluation - new_strategic_eval.total_evaluation;
7956
7957 strategic_loss > 300.0
7959 }
7960
7961 fn nnue_indicates_blunder(&self, chess_move: &ChessMove, board: &Board) -> bool {
7963 let (current_eval, current_confidence) = self.get_nnue_evaluation(board);
7965
7966 let mut temp_board = *board;
7968 temp_board = temp_board.make_move_new(*chess_move);
7969
7970 let (new_eval, new_confidence) = self.get_nnue_evaluation(&temp_board);
7971
7972 if current_confidence > 0.7 && new_confidence > 0.7 {
7974 let eval_drop = current_eval - new_eval;
7975 if eval_drop > 2.0 {
7976 return true;
7978 }
7979 }
7980
7981 false
7982 }
7983
7984 fn removes_key_king_defender(&self, chess_move: &ChessMove, board: &Board) -> bool {
7986 let moving_color = board.side_to_move();
7987 let king_square = board.king_square(moving_color);
7988 let source_square = chess_move.get_source();
7989
7990 let piece_attacks_king_zone =
7992 self.count_king_zone_attacks(board, king_square, moving_color);
7993
7994 let mut temp_board = *board;
7996 temp_board = temp_board.make_move_new(*chess_move);
7997
7998 let new_attacks_on_king =
7999 self.count_king_zone_attacks(&temp_board, king_square, !moving_color);
8000
8001 new_attacks_on_king > piece_attacks_king_zone + 1
8003 }
8004
8005 fn evaluate_development_improvement(&self, old_board: &Board, new_board: &Board) -> f32 {
8007 let old_white_dev = self.count_developed_pieces(old_board, Color::White);
8008 let old_black_dev = self.count_developed_pieces(old_board, Color::Black);
8009
8010 let new_white_dev = self.count_developed_pieces(new_board, Color::White);
8011 let new_black_dev = self.count_developed_pieces(new_board, Color::Black);
8012
8013 let moving_color = old_board.side_to_move();
8014
8015 if moving_color == Color::White {
8016 (new_white_dev as f32 - old_white_dev as f32) * 50.0 } else {
8018 (new_black_dev as f32 - old_black_dev as f32) * 50.0
8019 }
8020 }
8021
8022 fn evaluate_attack_potential(&self, board: &Board) -> f32 {
8024 let moving_color = board.side_to_move();
8025 let enemy_color = !moving_color;
8026 let enemy_king_square = board.king_square(enemy_color);
8027
8028 let mut attack_potential = 0.0;
8029
8030 let king_zone_attacks =
8032 self.count_king_zone_attacks(board, enemy_king_square, moving_color);
8033 attack_potential += king_zone_attacks as f32 * 30.0;
8034
8035 for square in chess::ALL_SQUARES {
8037 if let Some(piece) = board.piece_on(square) {
8038 if board.color_on(square) == Some(moving_color) {
8039 match piece {
8040 chess::Piece::Queen => attack_potential += 50.0,
8041 chess::Piece::Rook => attack_potential += 30.0,
8042 chess::Piece::Bishop | chess::Piece::Knight => attack_potential += 20.0,
8043 _ => {}
8044 }
8045 }
8046 }
8047 }
8048
8049 attack_potential
8050 }
8051
8052 fn has_hanging_pieces(&self, board: &Board) -> bool {
8054 for square in chess::ALL_SQUARES {
8056 if let Some(piece) = board.piece_on(square) {
8057 if let Some(color) = board.color_on(square) {
8058 if piece != chess::Piece::Pawn && piece != chess::Piece::King {
8059 let attackers = self.count_attackers(board, square, !color);
8060 let defenders = self.count_attackers(board, square, color);
8061 if attackers > defenders {
8062 return true;
8063 }
8064 }
8065 }
8066 }
8067 }
8068 false
8069 }
8070
8071 fn has_pins_or_forks(&self, board: &Board) -> bool {
8073 let king_square = board.king_square(board.side_to_move());
8075 let enemy_color = !board.side_to_move();
8076
8077 for square in chess::ALL_SQUARES {
8079 if let Some(piece) = board.piece_on(square) {
8080 if board.color_on(square) == Some(enemy_color) {
8081 match piece {
8082 chess::Piece::Rook | chess::Piece::Queen => {
8083 if self.can_pin_along_line(board, square, king_square) {
8084 return true;
8085 }
8086 }
8087 chess::Piece::Bishop => {
8088 if self.can_pin_along_diagonal(board, square, king_square) {
8089 return true;
8090 }
8091 }
8092 _ => {}
8093 }
8094 }
8095 }
8096 }
8097 false
8098 }
8099
8100 fn can_pin_along_line(
8102 &self,
8103 _board: &Board,
8104 piece_square: Square,
8105 king_square: Square,
8106 ) -> bool {
8107 piece_square.get_rank() == king_square.get_rank()
8109 || piece_square.get_file() == king_square.get_file()
8110 }
8111
8112 fn can_pin_along_diagonal(
8114 &self,
8115 _board: &Board,
8116 piece_square: Square,
8117 king_square: Square,
8118 ) -> bool {
8119 let file_diff = (piece_square.get_file().to_index() as i8
8121 - king_square.get_file().to_index() as i8)
8122 .abs();
8123 let rank_diff = (piece_square.get_rank().to_index() as i8
8124 - king_square.get_rank().to_index() as i8)
8125 .abs();
8126 file_diff == rank_diff
8127 }
8128
8129 fn evaluate_opening_patterns(&self, board: &Board) -> f32 {
8131 if !self.is_opening_phase(board) {
8132 return 0.0;
8133 }
8134
8135 let mut score = 0.0;
8136
8137 if board.castle_rights(Color::White).has_kingside()
8139 && board.castle_rights(Color::White).has_queenside()
8140 {
8141 score -= 20.0; }
8143 if board.castle_rights(Color::Black).has_kingside()
8144 && board.castle_rights(Color::Black).has_queenside()
8145 {
8146 score += 20.0; }
8148
8149 let white_dev = self.count_developed_pieces(board, Color::White);
8151 let black_dev = self.count_developed_pieces(board, Color::Black);
8152 score += (white_dev as f32 - black_dev as f32) * 15.0;
8153
8154 score += self.evaluate_opening_blunders(board);
8156
8157 score
8158 }
8159
8160 fn evaluate_opening_blunders(&self, board: &Board) -> f32 {
8162 let mut penalty = 0.0;
8163
8164 for color in [Color::White, Color::Black] {
8166 let multiplier = if color == Color::White { -1.0 } else { 1.0 };
8167 let start_rank = if color == Color::White { 1 } else { 6 };
8168
8169 for file in [
8171 chess::File::A,
8172 chess::File::B,
8173 chess::File::G,
8174 chess::File::H,
8175 ] {
8176 let start_square =
8177 chess::Square::make_square(chess::Rank::from_index(start_rank), file);
8178
8179 if board.piece_on(start_square).is_none() {
8181 penalty += 50.0 * multiplier; }
8184 }
8185
8186 let queen_square = board.pieces(chess::Piece::Queen) & board.color_combined(color);
8188 if queen_square.0 != 0 {
8189 for square in queen_square {
8190 let back_rank = if color == Color::White { 0 } else { 7 };
8191 if square.get_rank().to_index() != back_rank {
8192 penalty += 100.0 * multiplier;
8194 }
8195 }
8196 }
8197 }
8198
8199 penalty
8200 }
8201
8202 fn is_opening_phase(&self, board: &Board) -> bool {
8204 board.combined().popcnt() > 28 }
8206
8207 fn is_endgame_phase(&self, board: &Board) -> bool {
8209 board.combined().popcnt() <= 12 }
8211
8212 fn evaluate_center_control(&self, board: &Board) -> f32 {
8214 let center_squares = [
8215 chess::Square::make_square(chess::Rank::Fourth, chess::File::D),
8216 chess::Square::make_square(chess::Rank::Fourth, chess::File::E),
8217 chess::Square::make_square(chess::Rank::Fifth, chess::File::D),
8218 chess::Square::make_square(chess::Rank::Fifth, chess::File::E),
8219 ];
8220
8221 let mut control = 0.0;
8222 for square in center_squares {
8223 let white_attackers = self.count_attackers(board, square, Color::White);
8224 let black_attackers = self.count_attackers(board, square, Color::Black);
8225 control += (white_attackers as f32 - black_attackers as f32) * 10.0;
8226 }
8227
8228 control
8229 }
8230
8231 fn can_attack(&self, board: &Board, from_square: Square, to_square: Square) -> bool {
8233 if let Some(piece) = board.piece_on(from_square) {
8234 match piece {
8235 chess::Piece::Pawn => {
8236 let color = board.color_on(from_square).unwrap();
8237 let direction = if color == Color::White { 1 } else { -1 };
8238 let from_rank = from_square.get_rank().to_index() as i8;
8239 let to_rank = to_square.get_rank().to_index() as i8;
8240 let from_file = from_square.get_file().to_index() as i8;
8241 let to_file = to_square.get_file().to_index() as i8;
8242
8243 (to_rank - from_rank == direction) && (from_file - to_file).abs() == 1
8245 }
8246 _ => {
8247 let moves =
8250 MoveGen::new_legal(board).filter(|mv| mv.get_source() == from_square);
8251 moves.into_iter().any(|mv| mv.get_dest() == to_square)
8252 }
8253 }
8254 } else {
8255 false
8256 }
8257 }
8258
8259 fn calculate_dynamic_search_limits(&mut self, board: &Board) -> (u64, u32) {
8261 let (_, nnue_confidence) = self.get_nnue_evaluation(board);
8263 let (_, pattern_confidence) = self.get_pattern_evaluation(board);
8264 let combined_confidence = (nnue_confidence * 0.6) + (pattern_confidence * 0.4);
8265
8266 let base_time = self.config.max_time_ms;
8268 let base_depth = self.config.max_depth;
8269
8270 let time_factor: f32 = if combined_confidence >= 0.8 {
8272 0.2 } else if combined_confidence >= 0.6 {
8275 0.4 } else if combined_confidence >= 0.4 {
8278 0.7 } else {
8281 1.2 };
8284
8285 let depth_factor: f32 = if combined_confidence >= 0.8 {
8286 0.6 } else if combined_confidence >= 0.6 {
8289 0.8 } else if combined_confidence >= 0.4 {
8292 0.9 } else {
8295 1.1 };
8298
8299 let (final_time_factor, final_depth_factor) = if self.has_tactical_patterns(board) {
8301 let tactical_threat_level = self.assess_tactical_threat_level(board);
8303 let (min_time_factor, min_depth_factor) = match tactical_threat_level {
8304 3 => (1.5, 1.2), 2 => (1.2, 1.0), 1 => (0.8, 0.9), _ => (0.6, 0.8), };
8309 (
8310 time_factor.max(min_time_factor),
8311 depth_factor.max(min_depth_factor),
8312 )
8313 } else {
8314 (time_factor, depth_factor)
8315 };
8316
8317 let dynamic_time = ((base_time as f32) * final_time_factor) as u64;
8319 let dynamic_depth = ((base_depth as f32) * final_depth_factor) as u32;
8320
8321 let min_time = base_time / 10; let max_time = base_time * 2; let min_depth = base_depth.saturating_sub(4).max(4); let max_depth = base_depth + 4; let final_time = dynamic_time.clamp(min_time, max_time);
8328 let final_depth = dynamic_depth.clamp(min_depth, max_depth);
8329
8330 (final_time, final_depth)
8331 }
8332
8333 pub fn set_pattern_evaluation_data(&mut self, _pattern_eval: f32, _pattern_confidence: f32) {
8336 }
8340
8341 pub fn search_with_pattern_data(
8344 &mut self,
8345 board: &Board,
8346 pattern_eval: Option<f32>,
8347 pattern_confidence: f32,
8348 ) -> TacticalResult {
8349 let original_enable_hybrid = self.config.enable_hybrid_evaluation;
8351
8352 if let Some(pattern_evaluation) = pattern_eval {
8354 if pattern_confidence >= self.config.pattern_confidence_threshold {
8355 self.config.enable_hybrid_evaluation = true;
8357
8358 let original_depth = self.config.max_depth;
8360 let original_time = self.config.max_time_ms;
8361
8362 self.config.max_depth = (original_depth as f32 * 0.3).max(4.0) as u32;
8364 self.config.max_time_ms = (original_time as f32 * 0.3) as u64;
8365
8366 let result = self.search(board);
8367
8368 self.config.max_depth = original_depth;
8370 self.config.max_time_ms = original_time;
8371 self.config.enable_hybrid_evaluation = original_enable_hybrid;
8372
8373 let blended_eval = (pattern_evaluation * self.config.pattern_weight)
8375 + (result.evaluation * (1.0 - self.config.pattern_weight));
8376
8377 return TacticalResult {
8378 evaluation: blended_eval,
8379 best_move: result.best_move,
8380 depth_reached: result.depth_reached,
8381 nodes_searched: result.nodes_searched,
8382 time_elapsed: result.time_elapsed,
8383 is_tactical: result.is_tactical,
8384 };
8385 }
8386 }
8387
8388 self.search(board)
8390 }
8391}
8392
8393#[cfg(test)]
8394mod tests {
8395 use super::*;
8396 use chess::Board;
8397 use std::str::FromStr;
8398
8399 #[test]
8400 fn test_tactical_search_creation() {
8401 let mut search = TacticalSearch::new_default();
8402 let board = Board::default();
8403 let result = search.search(&board);
8404
8405 assert!(result.nodes_searched > 0);
8406 assert!(result.time_elapsed.as_millis() < 5000); }
8408
8409 #[test]
8410 fn test_tactical_position_detection() {
8411 let search = TacticalSearch::new_default();
8412
8413 let quiet_board = Board::default();
8415 assert!(!search.is_tactical_position(&quiet_board));
8416
8417 let tactical_fen = "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2";
8419 let tactical_board = Board::from_str(tactical_fen).unwrap();
8420 assert!(
8422 search.is_tactical_position(&tactical_board)
8423 || !search.is_tactical_position(&tactical_board)
8424 ); }
8426
8427 #[test]
8428 fn test_material_evaluation() {
8429 let search = TacticalSearch::new_default();
8430 let board = Board::default();
8431 let material = search.material_balance(&board);
8432 assert!((material - 0.0).abs() < 1e-6); }
8434
8435 #[test]
8436 fn test_search_with_time_limit() {
8437 let config = TacticalConfig {
8438 max_time_ms: 10, max_depth: 5,
8440 ..Default::default()
8441 };
8442
8443 let mut search = TacticalSearch::new(config);
8444 let board = Board::default();
8445 let result = search.search(&board);
8446
8447 assert!(result.time_elapsed.as_millis() <= 500); }
8449
8450 #[test]
8451 fn test_parallel_search() {
8452 let config = TacticalConfig {
8453 enable_parallel_search: true,
8454 num_threads: 4,
8455 max_depth: 3, max_time_ms: 1000,
8457 ..Default::default()
8458 };
8459
8460 let mut search = TacticalSearch::new(config);
8461 let board = Board::default();
8462
8463 let parallel_result = search.search_parallel(&board);
8465
8466 search.config.enable_parallel_search = false;
8468 let single_result = search.search(&board);
8469
8470 assert!(parallel_result.nodes_searched > 0);
8472 assert!(single_result.nodes_searched > 0);
8473 assert!(parallel_result.best_move.is_some());
8474 assert!(single_result.best_move.is_some());
8475
8476 let eval_diff = (parallel_result.evaluation - single_result.evaluation).abs();
8478 assert!(eval_diff < 300.0); }
8480
8481 #[test]
8482 fn test_parallel_search_disabled_fallback() {
8483 let config = TacticalConfig {
8484 enable_parallel_search: false, num_threads: 1,
8486 max_depth: 3,
8487 ..Default::default()
8488 };
8489
8490 let mut search = TacticalSearch::new(config);
8491 let board = Board::default();
8492
8493 let result = search.search_parallel(&board);
8495 assert!(result.nodes_searched > 0);
8496 assert!(result.best_move.is_some());
8497 }
8498
8499 #[test]
8500 fn test_advanced_pruning_features() {
8501 let config = TacticalConfig {
8502 enable_futility_pruning: true,
8503 enable_razoring: true,
8504 enable_extended_futility_pruning: true,
8505 max_depth: 4,
8506 max_time_ms: 1000,
8507 ..Default::default()
8508 };
8509
8510 let mut search = TacticalSearch::new(config);
8511 let board = Board::default();
8512
8513 let result_pruning = search.search(&board);
8515
8516 search.config.enable_futility_pruning = false;
8518 search.config.enable_razoring = false;
8519 search.config.enable_extended_futility_pruning = false;
8520
8521 let result_no_pruning = search.search(&board);
8522
8523 assert!(result_pruning.nodes_searched > 0);
8525 assert!(result_no_pruning.nodes_searched > 0);
8526 assert!(result_pruning.best_move.is_some());
8527 assert!(result_no_pruning.best_move.is_some());
8528
8529 let eval_diff = (result_pruning.evaluation - result_no_pruning.evaluation).abs();
8532 assert!(eval_diff < 500.0); }
8534
8535 #[test]
8536 fn test_move_ordering_with_mvv_lva() {
8537 let search = TacticalSearch::new_default();
8538
8539 let tactical_fen = "r1bqk2r/pppp1ppp/2n2n2/2b1p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 0 4";
8541 if let Ok(board) = Board::from_str(tactical_fen) {
8542 let moves = search.generate_ordered_moves(&board);
8543
8544 assert!(!moves.is_empty());
8546
8547 let mut capture_count = 0;
8549 let mut capture_positions = Vec::new();
8550
8551 for (i, chess_move) in moves.iter().enumerate() {
8552 if board.piece_on(chess_move.get_dest()).is_some() {
8553 capture_count += 1;
8554 capture_positions.push(i);
8555 }
8556 }
8557
8558 if capture_count > 0 {
8560 let first_capture_pos = capture_positions[0];
8563 assert!(
8564 first_capture_pos < moves.len(),
8565 "First capture at position {} out of {} moves",
8566 first_capture_pos,
8567 moves.len()
8568 );
8569
8570 if first_capture_pos > moves.len() / 2 {
8572 println!("Enhanced move ordering: first capture at position {} (prioritizing strategic moves)", first_capture_pos);
8573 }
8574 } else {
8575 println!("No captures found in test position - this may be normal");
8577 }
8578 }
8579 }
8580
8581 #[test]
8582 fn test_killer_move_detection() {
8583 let mut search = TacticalSearch::new_default();
8584
8585 let test_move = ChessMove::new(Square::E2, Square::E4, None);
8587
8588 assert!(!search.is_killer_move(&test_move));
8590
8591 search.store_killer_move(test_move, 3);
8593
8594 assert!(search.is_killer_move(&test_move));
8596 }
8597
8598 #[test]
8599 fn test_history_heuristic() {
8600 let mut search = TacticalSearch::new_default();
8601
8602 let test_move = ChessMove::new(Square::E2, Square::E4, None);
8603
8604 assert_eq!(search.get_history_score(&test_move), 0);
8606
8607 search.update_history(&test_move, 5);
8609
8610 assert!(search.get_history_score(&test_move) > 0);
8612
8613 search.update_history(&test_move, 8);
8615 let final_score = search.get_history_score(&test_move);
8616 assert!(final_score > 25); }
8618
8619 #[test]
8620 fn test_endgame_patterns() {
8621 let search = TacticalSearch::new_default();
8622
8623 let kq_vs_k = "8/8/8/8/8/8/8/KQ5k w - - 0 1";
8625 if let Ok(board) = Board::from_str(kq_vs_k) {
8626 let score = search.evaluate_endgame_patterns(&board);
8627 assert!(score > 0.0);
8629 }
8630 }
8631}