1use chess::{Board, ChessMove, Color, MoveGen, Square};
2use rayon::prelude::*;
3use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9enum GamePhase {
10 Opening,
11 Middlegame,
12 Endgame,
13}
14
15#[derive(Clone)]
17struct FixedTranspositionTable {
18 entries: Vec<Option<TranspositionEntry>>,
19 size: usize,
20 age: u8,
21}
22
23impl FixedTranspositionTable {
24 fn new(size_mb: usize) -> Self {
25 let entry_size = std::mem::size_of::<TranspositionEntry>();
26 let size = (size_mb * 1024 * 1024) / entry_size;
27
28 Self {
29 entries: vec![None; size],
30 size,
31 age: 0,
32 }
33 }
34
35 fn get(&self, hash: u64) -> Option<&TranspositionEntry> {
36 let index = (hash as usize) % self.size;
37 self.entries[index].as_ref()
38 }
39
40 fn insert(&mut self, hash: u64, entry: TranspositionEntry) {
41 let index = (hash as usize) % self.size;
42
43 let should_replace = match &self.entries[index] {
45 None => true,
46 Some(existing) => {
47 entry.depth >= existing.depth || (self.age.wrapping_sub(existing.age) > 4)
49 }
50 };
51
52 if should_replace {
53 self.entries[index] = Some(TranspositionEntry {
54 age: self.age,
55 ..entry
56 });
57 }
58 }
59
60 fn clear(&mut self) {
61 self.entries.fill(None);
62 self.age = self.age.wrapping_add(1);
63 }
64
65 fn len(&self) -> usize {
66 self.entries.iter().filter(|e| e.is_some()).count()
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct TacticalResult {
73 pub evaluation: f32,
74 pub best_move: Option<ChessMove>,
75 pub depth_reached: u32,
76 pub nodes_searched: u64,
77 pub time_elapsed: Duration,
78 pub is_tactical: bool,
79}
80
81#[derive(Debug, Clone)]
83pub struct TacticalConfig {
84 pub max_depth: u32,
86 pub max_time_ms: u64,
87 pub max_nodes: u64,
88 pub quiescence_depth: u32,
89
90 pub enable_transposition_table: bool,
92 pub enable_iterative_deepening: bool,
93 pub enable_aspiration_windows: bool,
94 pub enable_null_move_pruning: bool,
95 pub enable_late_move_reductions: bool,
96 pub enable_principal_variation_search: bool,
97 pub enable_parallel_search: bool,
98 pub num_threads: usize,
99
100 pub enable_futility_pruning: bool,
102 pub enable_razoring: bool,
103 pub enable_extended_futility_pruning: bool,
104 pub futility_margin_base: f32,
105 pub razor_margin: f32,
106 pub extended_futility_margin: f32,
107
108 pub null_move_reduction_depth: u32,
110 pub lmr_min_depth: u32,
111 pub lmr_min_moves: usize,
112 pub aspiration_window_size: f32,
113 pub aspiration_max_iterations: u32,
114 pub transposition_table_size_mb: usize,
115 pub killer_move_slots: usize,
116 pub history_max_depth: u32,
117
118 pub time_allocation_factor: f32,
120 pub time_extension_threshold: f32,
121 pub panic_time_factor: f32,
122
123 pub endgame_evaluation_weight: f32,
125 pub mobility_weight: f32,
126 pub king_safety_weight: f32,
127 pub pawn_structure_weight: f32,
128
129 pub enable_check_extensions: bool,
131 pub check_extension_depth: u32,
132 pub max_extensions_per_line: u32,
133
134 pub enable_hybrid_evaluation: bool, pub hybrid_evaluation_weight: f32, pub hybrid_move_ordering: bool, pub hybrid_pruning_threshold: f32, }
140
141impl Default for TacticalConfig {
142 fn default() -> Self {
143 Self {
144 max_depth: 14, max_time_ms: 5000, max_nodes: 2_000_000, quiescence_depth: 12, enable_transposition_table: true,
152 enable_iterative_deepening: true,
153 enable_aspiration_windows: true, enable_null_move_pruning: true,
155 enable_late_move_reductions: true,
156 enable_principal_variation_search: true,
157 enable_parallel_search: true,
158 num_threads: 4,
159
160 enable_futility_pruning: true,
162 enable_razoring: true,
163 enable_extended_futility_pruning: true,
164 futility_margin_base: 200.0, razor_margin: 400.0, extended_futility_margin: 60.0, null_move_reduction_depth: 3, lmr_min_depth: 2, lmr_min_moves: 3, 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,
196 hybrid_evaluation_weight: 0.7, hybrid_move_ordering: false, hybrid_pruning_threshold: 0.5, }
200 }
201}
202
203impl TacticalConfig {
204 pub fn hybrid_optimized() -> Self {
206 Self {
207 max_depth: 10, max_time_ms: 1500, max_nodes: 1_000_000, quiescence_depth: 8, 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,
229 hybrid_evaluation_weight: 0.8, hybrid_move_ordering: true, hybrid_pruning_threshold: 0.6, ..Default::default()
234 }
235 }
236
237 pub fn nnue_assisted_fast() -> Self {
239 Self {
240 max_depth: 6, max_time_ms: 500, max_nodes: 100_000, quiescence_depth: 4, futility_margin_base: 100.0,
247 razor_margin: 200.0,
248 extended_futility_margin: 30.0,
249
250 aspiration_window_size: 60.0,
252 aspiration_max_iterations: 2,
253
254 time_allocation_factor: 0.2,
256 time_extension_threshold: 1.5,
257 panic_time_factor: 1.2,
258
259 enable_hybrid_evaluation: true,
261 hybrid_evaluation_weight: 0.9, hybrid_move_ordering: true, hybrid_pruning_threshold: 0.8, ..Default::default()
266 }
267 }
268
269 pub fn fast() -> Self {
271 Self {
272 max_depth: 8,
273 max_time_ms: 1000,
274 max_nodes: 200_000,
275 quiescence_depth: 4,
276 aspiration_window_size: 75.0,
277 transposition_table_size_mb: 32,
278 ..Default::default()
279 }
280 }
281
282 pub fn strong() -> Self {
284 Self {
285 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()
293 }
294 }
295
296 pub fn analysis() -> Self {
298 Self {
299 max_depth: 20,
300 max_time_ms: 60_000, max_nodes: 10_000_000, quiescence_depth: 10,
303 enable_aspiration_windows: false, transposition_table_size_mb: 512,
305 num_threads: std::thread::available_parallelism()
306 .map(|n| n.get())
307 .unwrap_or(4),
308 ..Default::default()
309 }
310 }
311
312 pub fn stockfish_optimized() -> Self {
314 Self {
315 max_depth: 12, max_time_ms: 2000, max_nodes: 1_000_000, quiescence_depth: 8, enable_transposition_table: true,
323 enable_iterative_deepening: true,
324 enable_aspiration_windows: true,
325 enable_null_move_pruning: true,
326 enable_late_move_reductions: true,
327 enable_principal_variation_search: true,
328 enable_parallel_search: true,
329 num_threads: 4, enable_futility_pruning: true,
333 enable_razoring: true,
334 enable_extended_futility_pruning: true,
335 futility_margin_base: 250.0, razor_margin: 500.0, extended_futility_margin: 80.0, 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,
362 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, }
371 }
372}
373
374#[derive(Debug, Clone)]
376struct TranspositionEntry {
377 depth: u32,
378 evaluation: f32,
379 best_move: Option<ChessMove>,
380 node_type: NodeType,
381 age: u8, }
383
384#[derive(Debug, Clone, Copy)]
385enum NodeType {
386 Exact,
387 LowerBound,
388 UpperBound,
389}
390
391#[derive(Clone)]
393pub struct TacticalSearch {
394 pub config: TacticalConfig,
395 transposition_table: FixedTranspositionTable,
396 nodes_searched: u64,
397 start_time: Instant,
398 killer_moves: Vec<Vec<Option<ChessMove>>>, history_heuristic: HashMap<(Square, Square), u32>,
402 counter_moves: HashMap<(Square, Square), ChessMove>,
404 last_move: Option<ChessMove>,
406}
407
408impl TacticalSearch {
409 pub fn new(config: TacticalConfig) -> Self {
411 let max_depth = config.max_depth as usize + 1;
412 Self {
413 config,
414 transposition_table: FixedTranspositionTable::new(64), nodes_searched: 0,
416 start_time: Instant::now(),
417 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
419 counter_moves: HashMap::new(),
420 last_move: None,
421 }
422 }
423
424 pub fn with_table_size(config: TacticalConfig, table_size_mb: usize) -> Self {
426 let max_depth = config.max_depth as usize + 1;
427 Self {
428 config,
429 transposition_table: FixedTranspositionTable::new(table_size_mb),
430 nodes_searched: 0,
431 start_time: Instant::now(),
432 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
434 counter_moves: HashMap::new(),
435 last_move: None,
436 }
437 }
438
439 pub fn new_default() -> Self {
441 Self::new(TacticalConfig::default())
442 }
443
444 pub fn search(&mut self, board: &Board) -> TacticalResult {
446 self.nodes_searched = 0;
447 self.start_time = Instant::now();
448 self.transposition_table.clear();
449
450 let is_tactical = self.is_tactical_position(board);
452
453 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
454 self.iterative_deepening_search(board)
455 } else {
456 let (eval, mv) = self.minimax(
457 board,
458 self.config.max_depth,
459 f32::NEG_INFINITY,
460 f32::INFINITY,
461 board.side_to_move() == Color::White,
462 );
463 (eval, mv, self.config.max_depth)
464 };
465
466 TacticalResult {
467 evaluation,
468 best_move,
469 depth_reached,
470 nodes_searched: self.nodes_searched,
471 time_elapsed: self.start_time.elapsed(),
472 is_tactical,
473 }
474 }
475
476 pub fn search_parallel(&mut self, board: &Board) -> TacticalResult {
478 if !self.config.enable_parallel_search || self.config.num_threads <= 1 {
479 return self.search(board); }
481
482 self.nodes_searched = 0;
483 self.start_time = Instant::now();
484 self.transposition_table.clear();
485
486 let is_tactical = self.is_tactical_position(board);
487 let moves = self.generate_ordered_moves(board);
488
489 if moves.is_empty() {
490 return TacticalResult {
491 evaluation: self.evaluate_terminal_position(board),
492 best_move: None,
493 depth_reached: 1,
494 nodes_searched: 1,
495 time_elapsed: self.start_time.elapsed(),
496 is_tactical,
497 };
498 }
499
500 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
502 self.parallel_iterative_deepening(board, moves)
503 } else {
504 self.parallel_root_search(board, moves, self.config.max_depth)
505 };
506
507 TacticalResult {
508 evaluation,
509 best_move,
510 depth_reached,
511 nodes_searched: self.nodes_searched,
512 time_elapsed: self.start_time.elapsed(),
513 is_tactical,
514 }
515 }
516
517 fn parallel_root_search(
519 &mut self,
520 board: &Board,
521 moves: Vec<ChessMove>,
522 depth: u32,
523 ) -> (f32, Option<ChessMove>, u32) {
524 let maximizing = board.side_to_move() == Color::White;
525 let nodes_counter = Arc::new(Mutex::new(0u64));
526
527 let move_scores: Vec<(ChessMove, f32)> = moves
529 .into_par_iter()
530 .map(|mv| {
531 let new_board = board.make_move_new(mv);
532 let mut search_clone = self.clone();
533 search_clone.nodes_searched = 0;
534
535 let (eval, _) = search_clone.minimax(
536 &new_board,
537 depth - 1,
538 f32::NEG_INFINITY,
539 f32::INFINITY,
540 !maximizing,
541 );
542
543 if let Ok(mut counter) = nodes_counter.lock() {
545 *counter += search_clone.nodes_searched;
546 }
547
548 (mv, -eval)
550 })
551 .collect();
552
553 if let Ok(counter) = nodes_counter.lock() {
555 self.nodes_searched = *counter;
556 }
557
558 let best = move_scores
560 .into_iter()
561 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
562
563 match best {
564 Some((best_move, best_eval)) => (best_eval, Some(best_move), depth),
565 None => (0.0, None, depth),
566 }
567 }
568
569 fn parallel_iterative_deepening(
571 &mut self,
572 board: &Board,
573 mut moves: Vec<ChessMove>,
574 ) -> (f32, Option<ChessMove>, u32) {
575 let mut best_move: Option<ChessMove> = None;
576 let mut best_evaluation = 0.0;
577 let mut completed_depth = 0;
578
579 for depth in 1..=self.config.max_depth {
580 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128 {
582 break;
583 }
584
585 let (eval, mv, _) = self.parallel_root_search(board, moves.clone(), depth);
586
587 best_evaluation = eval;
588 best_move = mv;
589 completed_depth = depth;
590
591 if let Some(best) = best_move {
593 if let Some(pos) = moves.iter().position(|&m| m == best) {
594 moves.swap(0, pos);
595 }
596 }
597 }
598
599 (best_evaluation, best_move, completed_depth)
600 }
601
602 fn iterative_deepening_search(&mut self, board: &Board) -> (f32, Option<ChessMove>, u32) {
604 let mut best_move: Option<ChessMove> = None;
605 let mut best_evaluation = 0.0;
606 let mut completed_depth = 0;
607
608 let position_complexity = self.calculate_position_complexity(board);
610 let base_time_per_depth = self.config.max_time_ms as f32 / self.config.max_depth as f32;
611 let adaptive_time_factor = 0.5 + (position_complexity * 1.5); for depth in 1..=self.config.max_depth {
614 let depth_start_time = std::time::Instant::now();
615
616 let depth_time_budget = (base_time_per_depth
618 * adaptive_time_factor
619 * (1.0 + (depth as f32 - 1.0) * 0.3)) as u64;
620
621 let elapsed = self.start_time.elapsed().as_millis() as u64;
623 if elapsed + depth_time_budget > self.config.max_time_ms {
624 break;
626 }
627
628 let window_size = if self.config.enable_aspiration_windows && depth > 2 {
629 50.0 } else {
631 f32::INFINITY
632 };
633
634 let (evaluation, mv) = if self.config.enable_aspiration_windows && depth > 2 {
635 self.aspiration_window_search(board, depth, best_evaluation, window_size)
636 } else {
637 self.minimax(
638 board,
639 depth,
640 f32::NEG_INFINITY,
641 f32::INFINITY,
642 board.side_to_move() == Color::White,
643 )
644 };
645
646 best_evaluation = evaluation;
648 if mv.is_some() {
649 best_move = mv;
650 }
651 completed_depth = depth;
652
653 if evaluation.abs() > 9000.0 {
655 break;
656 }
657
658 if depth >= 8 && mv.is_some() && evaluation.abs() >= 5.0 {
661 break;
662 }
663
664 let depth_time_taken = depth_start_time.elapsed().as_millis() as u64;
667 let remaining_time = self
668 .config
669 .max_time_ms
670 .saturating_sub(elapsed + depth_time_taken);
671
672 if depth < self.config.max_depth {
674 let estimated_next_depth_time = depth_time_taken * 3; if estimated_next_depth_time > remaining_time {
676 break;
677 }
678 }
679 }
680
681 (best_evaluation, best_move, completed_depth)
682 }
683
684 fn calculate_position_complexity(&self, board: &Board) -> f32 {
686 let mut complexity = 0.0;
687
688 let total_pieces = board.combined().popcnt() as f32;
690 complexity += (total_pieces - 20.0) / 12.0; let legal_moves = MoveGen::new_legal(board).count() as f32;
694 complexity += (legal_moves - 20.0) / 20.0; if board.checkers().popcnt() > 0 {
698 complexity += 0.5;
699 }
700
701 if self.is_tactical_position(board) {
703 complexity += 0.3;
704 }
705
706 let game_phase = self.determine_game_phase(board);
708 if game_phase == GamePhase::Endgame {
709 complexity -= 0.3;
710 }
711
712 complexity.clamp(0.2, 1.5)
714 }
715
716 fn aspiration_window_search(
718 &mut self,
719 board: &Board,
720 depth: u32,
721 prev_score: f32,
722 window: f32,
723 ) -> (f32, Option<ChessMove>) {
724 let mut alpha = prev_score - window;
725 let mut beta = prev_score + window;
726
727 loop {
728 let (score, mv) = self.minimax(
729 board,
730 depth,
731 alpha,
732 beta,
733 board.side_to_move() == Color::White,
734 );
735
736 if score <= alpha {
737 alpha = f32::NEG_INFINITY;
739 } else if score >= beta {
740 beta = f32::INFINITY;
742 } else {
743 return (score, mv);
745 }
746 }
747 }
748
749 fn minimax(
751 &mut self,
752 board: &Board,
753 depth: u32,
754 alpha: f32,
755 beta: f32,
756 maximizing: bool,
757 ) -> (f32, Option<ChessMove>) {
758 self.minimax_with_extensions(board, depth, alpha, beta, maximizing, 0)
759 }
760
761 fn minimax_with_extensions(
762 &mut self,
763 board: &Board,
764 depth: u32,
765 alpha: f32,
766 beta: f32,
767 maximizing: bool,
768 extensions_used: u32,
769 ) -> (f32, Option<ChessMove>) {
770 self.nodes_searched += 1;
771
772 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128
774 || self.nodes_searched > self.config.max_nodes
775 {
776 return (self.evaluate_position(board), None);
777 }
778
779 let mut actual_depth = depth;
781 if self.config.enable_check_extensions
782 && board.checkers().popcnt() > 0
783 && extensions_used < self.config.max_extensions_per_line
784 {
785 actual_depth += self.config.check_extension_depth;
786 }
787
788 if actual_depth == 0 {
790 return (
791 self.quiescence_search(
792 board,
793 self.config.quiescence_depth,
794 alpha,
795 beta,
796 maximizing,
797 ),
798 None,
799 );
800 }
801
802 if board.status() != chess::BoardStatus::Ongoing {
803 return (self.evaluate_terminal_position(board), None);
804 }
805
806 if self.config.enable_transposition_table {
808 if let Some(entry) = self.transposition_table.get(board.get_hash()) {
809 if entry.depth >= depth {
810 match entry.node_type {
811 NodeType::Exact => return (entry.evaluation, entry.best_move),
812 NodeType::LowerBound if entry.evaluation >= beta => {
813 return (entry.evaluation, entry.best_move)
814 }
815 NodeType::UpperBound if entry.evaluation <= alpha => {
816 return (entry.evaluation, entry.best_move)
817 }
818 _ => {}
819 }
820 }
821 }
822 }
823
824 let static_eval = self.evaluate_position(board);
826
827 if self.config.enable_razoring
829 && (1..=3).contains(&depth)
830 && !maximizing && static_eval + self.config.razor_margin < alpha
832 {
833 let razor_eval = self.quiescence_search(board, 1, alpha, beta, maximizing);
835 if razor_eval < alpha {
836 return (razor_eval, None);
837 }
838 }
839
840 if self.config.enable_futility_pruning
842 && depth == 1
843 && !maximizing
844 && board.checkers().popcnt() == 0 && static_eval + self.config.futility_margin_base < alpha
846 {
847 return (static_eval, None);
849 }
850
851 if self.config.enable_extended_futility_pruning
853 && (2..=4).contains(&depth)
854 && !maximizing
855 && board.checkers().popcnt() == 0
856 {
858 let futility_margin = self.config.extended_futility_margin * (depth as f32);
859
860 if static_eval + futility_margin < alpha {
862 return (static_eval, None);
863 }
864
865 if static_eval + 500.0 < alpha && depth <= 3 {
867 return (static_eval, None);
868 }
869 }
870
871 if self.config.enable_null_move_pruning
873 && depth >= 3
874 && maximizing && board.checkers().popcnt() == 0 && self.has_non_pawn_material(board, board.side_to_move())
877 {
878 let null_move_reduction = (depth / 4).clamp(2, 4);
879 let new_depth = depth.saturating_sub(null_move_reduction);
880
881 let null_board = board.null_move().unwrap_or(*board);
883 let (null_score, _) = self.minimax(&null_board, new_depth, alpha, beta, !maximizing);
884
885 if null_score >= beta {
887 return (beta, None);
888 }
889 }
890
891 let hash_move = if self.config.enable_transposition_table {
893 self.transposition_table
894 .get(board.get_hash())
895 .and_then(|entry| entry.best_move)
896 } else {
897 None
898 };
899
900 let moves = self.generate_ordered_moves_with_hash(board, hash_move, depth);
902
903 let (best_value, best_move) =
904 if self.config.enable_principal_variation_search && moves.len() > 1 {
905 self.principal_variation_search(board, depth, alpha, beta, maximizing, moves)
907 } else {
908 self.alpha_beta_search(board, depth, alpha, beta, maximizing, moves)
910 };
911
912 if self.config.enable_transposition_table {
914 let node_type = if best_value <= alpha {
915 NodeType::UpperBound
916 } else if best_value >= beta {
917 NodeType::LowerBound
918 } else {
919 NodeType::Exact
920 };
921
922 self.transposition_table.insert(
923 board.get_hash(),
924 TranspositionEntry {
925 depth,
926 evaluation: best_value,
927 best_move,
928 node_type,
929 age: 0, },
931 );
932 }
933
934 (best_value, best_move)
935 }
936
937 fn principal_variation_search(
939 &mut self,
940 board: &Board,
941 depth: u32,
942 mut alpha: f32,
943 mut beta: f32,
944 maximizing: bool,
945 moves: Vec<ChessMove>,
946 ) -> (f32, Option<ChessMove>) {
947 let mut best_move: Option<ChessMove> = None;
948 let mut best_value = if maximizing {
949 f32::NEG_INFINITY
950 } else {
951 f32::INFINITY
952 };
953 let mut _pv_found = false;
954 let mut first_move = true;
955
956 if moves.is_empty() {
958 return (self.evaluate_position(board), None);
959 }
960
961 for (move_index, chess_move) in moves.into_iter().enumerate() {
962 let new_board = board.make_move_new(chess_move);
963 let mut evaluation;
964
965 let reduction = if self.config.enable_late_move_reductions
967 && depth >= 3
968 && move_index >= 2 && !self.is_capture_or_promotion(&chess_move, board)
970 && new_board.checkers().popcnt() == 0 && !self.is_killer_move(&chess_move)
972 {
973 let base_reduction = if move_index >= 6 { 2 } else { 1 };
977 let depth_factor = (depth as f32 / 3.0) as u32;
978 let move_factor = ((move_index as f32).ln() / 2.0) as u32;
979
980 base_reduction + depth_factor + move_factor
981 } else {
982 0
983 };
984
985 let search_depth = if depth > reduction {
986 depth - 1 - reduction
987 } else {
988 0
989 };
990
991 if move_index == 0 {
992 let search_depth = if depth > 0 { depth - 1 } else { 0 };
994 let (eval, _) = self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
995 evaluation = eval;
996 _pv_found = true;
997 } else {
998 let null_window_alpha = if maximizing { alpha } else { beta - 1.0 };
1000 let null_window_beta = if maximizing { alpha + 1.0 } else { beta };
1001
1002 let (null_eval, _) = self.minimax(
1003 &new_board,
1004 search_depth,
1005 null_window_alpha,
1006 null_window_beta,
1007 !maximizing,
1008 );
1009
1010 if null_eval > alpha && null_eval < beta {
1012 let full_depth = if reduction > 0 {
1014 if depth > 0 {
1015 depth - 1
1016 } else {
1017 0
1018 }
1019 } else {
1020 search_depth
1021 };
1022 let (full_eval, _) =
1023 self.minimax(&new_board, full_depth, alpha, beta, !maximizing);
1024 evaluation = full_eval;
1025 } else {
1026 evaluation = null_eval;
1027
1028 if reduction > 0
1030 && ((maximizing && evaluation > alpha)
1031 || (!maximizing && evaluation < beta))
1032 {
1033 let search_depth = if depth > 0 { depth - 1 } else { 0 };
1034 let (re_eval, _) =
1035 self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
1036 evaluation = re_eval;
1037 }
1038 }
1039 }
1040
1041 if maximizing {
1043 if first_move || evaluation > best_value {
1044 best_value = evaluation;
1045 best_move = Some(chess_move);
1046 }
1047 alpha = alpha.max(evaluation);
1048 } else {
1049 if first_move || evaluation < best_value {
1050 best_value = evaluation;
1051 best_move = Some(chess_move);
1052 }
1053 beta = beta.min(evaluation);
1054 }
1055
1056 first_move = false;
1057
1058 if beta <= alpha {
1060 if !self.is_capture_or_promotion(&chess_move, board) {
1062 self.store_killer_move(chess_move, depth);
1063 self.update_history(&chess_move, depth);
1064 }
1065 break;
1066 }
1067 }
1068
1069 (best_value, best_move)
1070 }
1071
1072 fn alpha_beta_search(
1074 &mut self,
1075 board: &Board,
1076 depth: u32,
1077 mut alpha: f32,
1078 mut beta: f32,
1079 maximizing: bool,
1080 moves: Vec<ChessMove>,
1081 ) -> (f32, Option<ChessMove>) {
1082 let mut best_move: Option<ChessMove> = None;
1083 let mut best_value = if maximizing {
1084 f32::NEG_INFINITY
1085 } else {
1086 f32::INFINITY
1087 };
1088 let mut first_move = true;
1089
1090 if moves.is_empty() {
1092 return (self.evaluate_position(board), None);
1093 }
1094
1095 for (move_index, chess_move) in moves.into_iter().enumerate() {
1096 let new_board = board.make_move_new(chess_move);
1097
1098 let reduction = if self.config.enable_late_move_reductions
1100 && depth >= 3
1101 && move_index >= 2 && !self.is_capture_or_promotion(&chess_move, board)
1103 && new_board.checkers().popcnt() == 0 && !self.is_killer_move(&chess_move)
1105 {
1106 let base_reduction = if move_index >= 6 { 2 } else { 1 };
1110 let depth_factor = (depth as f32 / 3.0) as u32;
1111 let move_factor = ((move_index as f32).ln() / 2.0) as u32;
1112
1113 base_reduction + depth_factor + move_factor
1114 } else {
1115 0
1116 };
1117
1118 let search_depth = if depth > reduction {
1119 depth - 1 - reduction
1120 } else {
1121 0
1122 };
1123
1124 let (evaluation, _) = self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
1125
1126 let final_evaluation = if reduction > 0
1128 && ((maximizing && evaluation > alpha) || (!maximizing && evaluation < beta))
1129 {
1130 let search_depth = if depth > 0 { depth - 1 } else { 0 };
1131 let (re_eval, _) = self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
1132 re_eval
1133 } else {
1134 evaluation
1135 };
1136
1137 if maximizing {
1138 if first_move || final_evaluation > best_value {
1139 best_value = final_evaluation;
1140 best_move = Some(chess_move);
1141 }
1142 alpha = alpha.max(final_evaluation);
1143 } else {
1144 if first_move || final_evaluation < best_value {
1145 best_value = final_evaluation;
1146 best_move = Some(chess_move);
1147 }
1148 beta = beta.min(final_evaluation);
1149 }
1150
1151 first_move = false;
1152
1153 if beta <= alpha {
1155 if !self.is_capture_or_promotion(&chess_move, board) {
1157 self.store_killer_move(chess_move, depth);
1158 self.update_history(&chess_move, depth);
1159 }
1160 break;
1161 }
1162 }
1163
1164 (best_value, best_move)
1165 }
1166
1167 fn quiescence_search(
1169 &mut self,
1170 board: &Board,
1171 depth: u32,
1172 mut alpha: f32,
1173 beta: f32,
1174 maximizing: bool,
1175 ) -> f32 {
1176 self.nodes_searched += 1;
1177
1178 let stand_pat = self.evaluate_position(board);
1179
1180 if depth == 0 {
1181 return stand_pat;
1182 }
1183
1184 if maximizing {
1185 if stand_pat >= beta {
1186 return beta;
1187 }
1188 alpha = alpha.max(stand_pat);
1189
1190 if stand_pat + 900.0 < alpha {
1192 return stand_pat;
1194 }
1195 } else {
1196 if stand_pat <= alpha {
1197 return alpha;
1198 }
1199
1200 if stand_pat - 900.0 > alpha {
1202 return stand_pat;
1204 }
1205 }
1206
1207 let captures_and_checks = self.generate_captures_and_checks(board);
1209
1210 for chess_move in captures_and_checks {
1211 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
1213 if !self.is_good_capture(&chess_move, board, captured_piece) {
1214 continue;
1216 }
1217 }
1218
1219 let new_board = board.make_move_new(chess_move);
1220 let evaluation =
1221 self.quiescence_search(&new_board, depth - 1, alpha, beta, !maximizing);
1222
1223 if maximizing {
1224 alpha = alpha.max(evaluation);
1225 if alpha >= beta {
1226 break;
1227 }
1228 } else if evaluation <= alpha {
1229 return alpha;
1230 }
1231 }
1232
1233 stand_pat
1234 }
1235
1236 fn generate_ordered_moves(&self, board: &Board) -> Vec<ChessMove> {
1238 self.generate_ordered_moves_with_hash(board, None, 1) }
1240
1241 fn generate_ordered_moves_with_hash(
1243 &self,
1244 board: &Board,
1245 hash_move: Option<ChessMove>,
1246 depth: u32,
1247 ) -> Vec<ChessMove> {
1248 let mut moves: Vec<_> = MoveGen::new_legal(board).collect();
1249
1250 moves.sort_by(|a, b| {
1252 let a_score = self.get_move_order_score(a, board, hash_move, depth);
1253 let b_score = self.get_move_order_score(b, board, hash_move, depth);
1254 b_score.cmp(&a_score) });
1256
1257 moves
1258 }
1259
1260 fn get_move_order_score(
1262 &self,
1263 chess_move: &ChessMove,
1264 board: &Board,
1265 hash_move: Option<ChessMove>,
1266 depth: u32,
1267 ) -> i32 {
1268 if let Some(hash) = hash_move {
1270 if hash == *chess_move {
1271 return 1_000_000; }
1273 }
1274
1275 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
1277 let mvv_lva_score = self.mvv_lva_score(chess_move, board);
1278
1279 let attacker_piece = board.piece_on(chess_move.get_source());
1281 if let Some(attacker) = attacker_piece {
1282 let material_exchange =
1283 self.calculate_material_exchange(chess_move, board, captured_piece, attacker);
1284
1285 if material_exchange < -150 {
1287 if material_exchange < -300 {
1289 let compensation = self.evaluate_sacrifice_compensation(chess_move, board);
1290 if compensation < material_exchange.abs() as f32 * 0.5 {
1291 return 200; }
1293 }
1294 return 500; }
1296
1297 if self.is_good_capture(chess_move, board, captured_piece) {
1299 return 900_000 + mvv_lva_score; } else {
1301 return 1_000 + mvv_lva_score; }
1303 }
1304 }
1305
1306 if chess_move.get_promotion().is_some() {
1308 let promotion_piece = chess_move.get_promotion().unwrap();
1309 let promotion_value = match promotion_piece {
1310 chess::Piece::Queen => 800_000,
1311 chess::Piece::Rook => 700_000,
1312 chess::Piece::Bishop => 600_000,
1313 chess::Piece::Knight => 590_000,
1314 _ => 500_000,
1315 };
1316 return promotion_value;
1317 }
1318
1319 if self.is_killer_move_at_depth(chess_move, depth) {
1321 return 500_000;
1322 }
1323
1324 if self.is_counter_move(chess_move) {
1326 return 400_000;
1327 }
1328
1329 if self.is_castling_move(chess_move, board) {
1331 return 250_000; }
1333
1334 if self.gives_check(chess_move, board) {
1336 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
1338 if let Some(attacker_piece) = board.piece_on(chess_move.get_source()) {
1340 let victim_value = self.get_piece_value(captured_piece);
1341 let attacker_value = self.get_piece_value(attacker_piece);
1342 if victim_value < attacker_value {
1343 return 5_000; }
1346 }
1347 return 300_000; } else {
1349 if let Some(_moving_piece) = board.piece_on(chess_move.get_source()) {
1352 let mut temp_board = *board;
1354 temp_board = temp_board.make_move_new(*chess_move);
1355
1356 let attackers = self.count_attackers(
1358 &temp_board,
1359 chess_move.get_dest(),
1360 !temp_board.side_to_move(),
1361 );
1362 let defenders = self.count_attackers(
1363 &temp_board,
1364 chess_move.get_dest(),
1365 temp_board.side_to_move(),
1366 );
1367
1368 if attackers > defenders {
1369 return 8_000;
1371 }
1372 }
1373 return 50_000; }
1375 }
1376
1377 let history_score = self.get_history_score(chess_move);
1379 200_000 + history_score as i32 }
1381
1382 fn is_good_capture(
1384 &self,
1385 chess_move: &ChessMove,
1386 board: &Board,
1387 captured_piece: chess::Piece,
1388 ) -> bool {
1389 let attacker_piece = board.piece_on(chess_move.get_source());
1390 if attacker_piece.is_none() {
1391 return false;
1392 }
1393
1394 let attacker_value = self.get_piece_value(attacker_piece.unwrap());
1395 let victim_value = self.get_piece_value(captured_piece);
1396
1397 let immediate_gain = victim_value - attacker_value;
1399
1400 if immediate_gain > 0 {
1402 return true;
1403 }
1404
1405 if immediate_gain < 0 {
1407 let defenders =
1409 self.count_attackers(board, chess_move.get_dest(), !board.side_to_move());
1410
1411 if defenders > 0 {
1413 return false;
1414 }
1415
1416 return immediate_gain >= -100; }
1419
1420 true
1422 }
1423
1424 fn get_piece_value(&self, piece: chess::Piece) -> i32 {
1426 match piece {
1427 chess::Piece::Pawn => 100,
1428 chess::Piece::Knight => 320,
1429 chess::Piece::Bishop => 330,
1430 chess::Piece::Rook => 500,
1431 chess::Piece::Queen => 900,
1432 chess::Piece::King => 10000,
1433 }
1434 }
1435
1436 fn calculate_material_exchange(
1438 &self,
1439 chess_move: &ChessMove,
1440 board: &Board,
1441 captured_piece: chess::Piece,
1442 attacker_piece: chess::Piece,
1443 ) -> i32 {
1444 let victim_value = self.get_piece_value(captured_piece);
1445 let attacker_value = self.get_piece_value(attacker_piece);
1446
1447 let immediate_gain = victim_value - attacker_value;
1449
1450 let dest_square = chess_move.get_dest();
1452 let opponent_attackers = self.count_attackers(board, dest_square, !board.side_to_move());
1453
1454 if opponent_attackers > 0 && attacker_value > victim_value {
1456 return victim_value - attacker_value;
1458 }
1459
1460 immediate_gain
1462 }
1463
1464 fn is_killer_move_at_depth(&self, chess_move: &ChessMove, depth: u32) -> bool {
1466 let depth_idx = (depth as usize).min(self.killer_moves.len() - 1);
1467 self.killer_moves[depth_idx].contains(&Some(*chess_move))
1468 }
1469
1470 fn is_counter_move(&self, chess_move: &ChessMove) -> bool {
1472 if let Some(last_move) = self.last_move {
1473 let last_move_key = (last_move.get_source(), last_move.get_dest());
1474 if let Some(counter_move) = self.counter_moves.get(&last_move_key) {
1475 return *counter_move == *chess_move;
1476 }
1477 }
1478 false
1479 }
1480
1481 fn is_castling_move(&self, chess_move: &ChessMove, board: &Board) -> bool {
1483 if let Some(piece) = board.piece_on(chess_move.get_source()) {
1484 if piece == chess::Piece::King {
1485 let source_file = chess_move.get_source().get_file().to_index();
1486 let dest_file = chess_move.get_dest().get_file().to_index();
1487 return (source_file as i32 - dest_file as i32).abs() == 2;
1489 }
1490 }
1491 false
1492 }
1493
1494 fn gives_check(&self, chess_move: &ChessMove, board: &Board) -> bool {
1496 let new_board = board.make_move_new(*chess_move);
1497 new_board.checkers().popcnt() > 0
1498 }
1499
1500 fn mvv_lva_score(&self, chess_move: &ChessMove, board: &Board) -> i32 {
1502 let victim_value = if let Some(victim_piece) = board.piece_on(chess_move.get_dest()) {
1503 match victim_piece {
1504 chess::Piece::Pawn => 100,
1505 chess::Piece::Knight => 300,
1506 chess::Piece::Bishop => 300,
1507 chess::Piece::Rook => 500,
1508 chess::Piece::Queen => 900,
1509 chess::Piece::King => 10000, }
1511 } else {
1512 0
1513 };
1514
1515 let attacker_value = if let Some(attacker_piece) = board.piece_on(chess_move.get_source()) {
1516 match attacker_piece {
1517 chess::Piece::Pawn => 1,
1518 chess::Piece::Knight => 3,
1519 chess::Piece::Bishop => 3,
1520 chess::Piece::Rook => 5,
1521 chess::Piece::Queen => 9,
1522 chess::Piece::King => 1, }
1524 } else {
1525 1
1526 };
1527
1528 victim_value * 10 - attacker_value
1530 }
1531
1532 fn generate_captures(&self, board: &Board) -> Vec<ChessMove> {
1534 MoveGen::new_legal(board)
1535 .filter(|chess_move| {
1536 board.piece_on(chess_move.get_dest()).is_some()
1538 || chess_move.get_promotion().is_some()
1539 })
1540 .collect()
1541 }
1542
1543 #[allow(dead_code)]
1545 fn generate_checks(&self, board: &Board) -> Vec<ChessMove> {
1546 MoveGen::new_legal(board)
1547 .filter(|chess_move| {
1548 let new_board = board.make_move_new(*chess_move);
1550 new_board.checkers().popcnt() > 0
1551 })
1552 .collect()
1553 }
1554
1555 fn generate_captures_and_checks(&self, board: &Board) -> Vec<ChessMove> {
1557 MoveGen::new_legal(board)
1558 .filter(|chess_move| {
1559 let is_capture = board.piece_on(chess_move.get_dest()).is_some();
1561 let is_promotion = chess_move.get_promotion().is_some();
1562 let is_check = if !is_capture && !is_promotion {
1563 let new_board = board.make_move_new(*chess_move);
1565 new_board.checkers().popcnt() > 0
1566 } else {
1567 false
1568 };
1569
1570 is_capture || is_promotion || is_check
1571 })
1572 .collect()
1573 }
1574
1575 fn evaluate_position(&self, board: &Board) -> f32 {
1577 if board.status() != chess::BoardStatus::Ongoing {
1578 return self.evaluate_terminal_position(board);
1579 }
1580
1581 let mut score = 0.0;
1582
1583 score += self.material_balance(board);
1585
1586 score += self.tactical_bonuses(board);
1588
1589 score += self.evaluate_hanging_pieces(board);
1591
1592 score += self.evaluate_material_safety(board);
1594
1595 score += self.king_safety(board);
1597
1598 score += self.evaluate_pawn_structure(board);
1600
1601 score += self.evaluate_endgame_patterns(board);
1603
1604 score
1608 }
1609
1610 fn evaluate_terminal_position(&self, board: &Board) -> f32 {
1612 match board.status() {
1613 chess::BoardStatus::Checkmate => {
1614 if board.side_to_move() == Color::White {
1615 -10.0 } else {
1617 10.0 }
1619 }
1620 chess::BoardStatus::Stalemate => 0.0,
1621 _ => 0.0,
1622 }
1623 }
1624
1625 fn material_balance(&self, board: &Board) -> f32 {
1627 let piece_values = [
1628 (chess::Piece::Pawn, 100.0),
1629 (chess::Piece::Knight, 320.0), (chess::Piece::Bishop, 330.0), (chess::Piece::Rook, 500.0),
1632 (chess::Piece::Queen, 900.0),
1633 ];
1634
1635 let mut balance = 0.0;
1636
1637 for (piece, value) in piece_values.iter() {
1638 let white_count = board.pieces(*piece) & board.color_combined(Color::White);
1639 let black_count = board.pieces(*piece) & board.color_combined(Color::Black);
1640
1641 balance += (white_count.popcnt() as f32 - black_count.popcnt() as f32) * value;
1642 }
1643
1644 balance += self.piece_square_evaluation(board);
1646
1647 balance / 100.0 }
1649
1650 fn piece_square_evaluation(&self, board: &Board) -> f32 {
1652 let mut score = 0.0;
1653 let game_phase = self.detect_game_phase(board);
1654
1655 let pawn_opening = [
1657 0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, 50, 50, 50, 50, 50, 10, 10, 20, 30, 30, 20, 10, 10,
1658 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,
1659 10, 10, -25, -25, 10, 10, 5, 0, 0, 0, 0, 0, 0, 0, 0,
1660 ];
1661
1662 let pawn_endgame = [
1663 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80, 50, 50, 50, 50, 50, 50, 50, 50,
1664 30, 30, 30, 30, 30, 30, 30, 30, 20, 20, 20, 20, 20, 20, 20, 20, 10, 10, 10, 10, 10, 10,
1665 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0,
1666 ];
1667
1668 let knight_opening = [
1670 -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 0, 0, 0, -20, -40, -30, 0, 10, 15,
1671 15, 10, 0, -30, -30, 5, 15, 20, 20, 15, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, -30, 5,
1672 10, 15, 15, 10, 5, -30, -40, -20, 0, 5, 5, 0, -20, -40, -50, -40, -30, -30, -30, -30,
1673 -40, -50,
1674 ];
1675
1676 let knight_endgame = [
1677 -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 5, 5, 0, -20, -40, -30, 0, 10, 15,
1678 15, 10, 0, -30, -30, 5, 15, 20, 20, 15, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, -30, 5,
1679 10, 15, 15, 10, 5, -30, -40, -20, 0, 5, 5, 0, -20, -40, -50, -40, -30, -30, -30, -30,
1680 -40, -50,
1681 ];
1682
1683 let bishop_opening = [
1685 -20, -10, -10, -10, -10, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 10, 10,
1686 5, 0, -10, -10, 5, 5, 10, 10, 5, 5, -10, -10, 0, 10, 10, 10, 10, 0, -10, -10, 10, 10,
1687 10, 10, 10, 10, -10, -10, 5, 0, 0, 0, 0, 5, -10, -20, -10, -10, -10, -10, -10, -10,
1688 -20,
1689 ];
1690
1691 let bishop_endgame = [
1692 -20, -10, -10, -10, -10, -10, -10, -20, -10, 5, 0, 0, 0, 0, 5, -10, -10, 0, 10, 15, 15,
1693 10, 0, -10, -10, 0, 15, 20, 20, 15, 0, -10, -10, 0, 15, 20, 20, 15, 0, -10, -10, 0, 10,
1694 15, 15, 10, 0, -10, -10, 5, 0, 0, 0, 0, 5, -10, -20, -10, -10, -10, -10, -10, -10, -20,
1695 ];
1696
1697 let rook_opening = [
1699 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,
1700 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,
1701 0, 0, -5, 0, 0, 0, 5, 5, 0, 0, 0,
1702 ];
1703
1704 let rook_endgame = [
1705 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,
1706 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,
1707 0, 0, 0, 0, 0, 0, 0, 0, 0,
1708 ];
1709
1710 let queen_opening = [
1712 -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 5, 5, 5,
1713 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,
1714 -10, 0, 5, 0, 0, 0, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20,
1715 ];
1716
1717 let queen_endgame = [
1718 -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 5, 5, 5, 5, 0, -10, -10, 5, 10, 10, 10,
1719 10, 5, -10, -5, 0, 10, 10, 10, 10, 0, -5, -5, 0, 10, 10, 10, 10, 0, -5, -10, 5, 10, 10,
1720 10, 10, 5, -10, -10, 0, 5, 5, 5, 5, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20,
1721 ];
1722
1723 let king_opening = [
1725 -30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, -30,
1726 -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, -20, -30,
1727 -30, -40, -40, -30, -30, -20, -10, -20, -20, -20, -20, -20, -20, -10, 20, 20, 0, 0, 0,
1728 0, 20, 20, 20, 30, 10, 0, 0, 10, 30, 20,
1729 ];
1730
1731 let king_endgame = [
1732 -50, -40, -30, -20, -20, -30, -40, -50, -30, -20, -10, 0, 0, -10, -20, -30, -30, -10,
1733 20, 30, 30, 20, -10, -30, -30, -10, 30, 40, 40, 30, -10, -30, -30, -10, 30, 40, 40, 30,
1734 -10, -30, -30, -10, 20, 30, 30, 20, -10, -30, -30, -30, 0, 0, 0, 0, -30, -30, -50, -30,
1735 -30, -30, -30, -30, -30, -50,
1736 ];
1737
1738 let phase_factor = match game_phase {
1740 GamePhase::Opening => 1.0,
1741 GamePhase::Middlegame => 0.5,
1742 GamePhase::Endgame => 0.0,
1743 };
1744
1745 for color in [Color::White, Color::Black] {
1747 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
1748
1749 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
1751 for square in pawns {
1752 let idx = if color == Color::White {
1753 square.to_index()
1754 } else {
1755 square.to_index() ^ 56
1756 };
1757 let opening_value = pawn_opening[idx] as f32;
1758 let endgame_value = pawn_endgame[idx] as f32;
1759 let interpolated_value =
1760 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1761 score += interpolated_value * multiplier * 0.01; }
1763
1764 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
1766 for square in knights {
1767 let idx = if color == Color::White {
1768 square.to_index()
1769 } else {
1770 square.to_index() ^ 56
1771 };
1772 let opening_value = knight_opening[idx] as f32;
1773 let endgame_value = knight_endgame[idx] as f32;
1774 let interpolated_value =
1775 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1776 score += interpolated_value * multiplier * 0.01;
1777 }
1778
1779 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
1781 for square in bishops {
1782 let idx = if color == Color::White {
1783 square.to_index()
1784 } else {
1785 square.to_index() ^ 56
1786 };
1787 let opening_value = bishop_opening[idx] as f32;
1788 let endgame_value = bishop_endgame[idx] as f32;
1789 let interpolated_value =
1790 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1791 score += interpolated_value * multiplier * 0.01;
1792 }
1793
1794 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
1796 for square in rooks {
1797 let idx = if color == Color::White {
1798 square.to_index()
1799 } else {
1800 square.to_index() ^ 56
1801 };
1802 let opening_value = rook_opening[idx] as f32;
1803 let endgame_value = rook_endgame[idx] as f32;
1804 let interpolated_value =
1805 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1806 score += interpolated_value * multiplier * 0.01;
1807 }
1808
1809 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
1811 for square in queens {
1812 let idx = if color == Color::White {
1813 square.to_index()
1814 } else {
1815 square.to_index() ^ 56
1816 };
1817 let opening_value = queen_opening[idx] as f32;
1818 let endgame_value = queen_endgame[idx] as f32;
1819 let interpolated_value =
1820 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1821 score += interpolated_value * multiplier * 0.01;
1822 }
1823
1824 let kings = board.pieces(chess::Piece::King) & board.color_combined(color);
1826 for square in kings {
1827 let idx = if color == Color::White {
1828 square.to_index()
1829 } else {
1830 square.to_index() ^ 56
1831 };
1832 let opening_value = king_opening[idx] as f32;
1833 let endgame_value = king_endgame[idx] as f32;
1834 let interpolated_value =
1835 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1836 score += interpolated_value * multiplier * 0.01;
1837 }
1838 }
1839
1840 score
1841 }
1842
1843 fn detect_game_phase(&self, board: &Board) -> GamePhase {
1845 let mut total_material = 0;
1846
1847 for color in [Color::White, Color::Black] {
1849 total_material +=
1850 (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt() * 9;
1851 total_material +=
1852 (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt() * 5;
1853 total_material +=
1854 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt() * 3;
1855 total_material +=
1856 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt() * 3;
1857 }
1858
1859 if total_material >= 60 {
1861 GamePhase::Opening
1862 } else if total_material >= 20 {
1863 GamePhase::Middlegame
1864 } else {
1865 GamePhase::Endgame
1866 }
1867 }
1868
1869 fn mobility_evaluation(&self, board: &Board) -> f32 {
1871 let mut score = 0.0;
1872
1873 for color in [Color::White, Color::Black] {
1874 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
1875 let mobility_score = self.calculate_piece_mobility(board, color);
1876 score += mobility_score * multiplier;
1877 }
1878
1879 score
1880 }
1881
1882 fn calculate_piece_mobility(&self, board: &Board, color: Color) -> f32 {
1884 let mut mobility = 0.0;
1885
1886 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
1888 for knight_square in knights {
1889 let knight_moves = self.count_knight_moves(board, knight_square, color);
1890 mobility += knight_moves as f32 * 4.0; }
1892
1893 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
1895 for bishop_square in bishops {
1896 let bishop_moves = self.count_bishop_moves(board, bishop_square, color);
1897 mobility += bishop_moves as f32 * 3.0; }
1899
1900 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
1902 for rook_square in rooks {
1903 let rook_moves = self.count_rook_moves(board, rook_square, color);
1904 mobility += rook_moves as f32 * 2.0; }
1906
1907 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
1909 for queen_square in queens {
1910 let queen_moves = self.count_queen_moves(board, queen_square, color);
1911 mobility += queen_moves as f32 * 1.0; }
1913
1914 let pawn_mobility = self.calculate_pawn_mobility(board, color);
1916 mobility += pawn_mobility * 5.0; mobility
1919 }
1920
1921 fn count_knight_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1923 let mut count = 0;
1924 let knight_offsets = [
1925 (-2, -1),
1926 (-2, 1),
1927 (-1, -2),
1928 (-1, 2),
1929 (1, -2),
1930 (1, 2),
1931 (2, -1),
1932 (2, 1),
1933 ];
1934
1935 let file = square.get_file().to_index() as i8;
1936 let rank = square.get_rank().to_index() as i8;
1937
1938 for (df, dr) in knight_offsets {
1939 let new_file = file + df;
1940 let new_rank = rank + dr;
1941
1942 if (0..8).contains(&new_file) && (0..8).contains(&new_rank) {
1943 let dest_square = Square::make_square(
1944 chess::Rank::from_index(new_rank as usize),
1945 chess::File::from_index(new_file as usize),
1946 );
1947 if let Some(_piece_on_dest) = board.piece_on(dest_square) {
1949 if board.color_on(dest_square) != Some(color) {
1950 count += 1; }
1952 } else {
1953 count += 1; }
1955 }
1956 }
1957
1958 count
1959 }
1960
1961 fn count_bishop_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1963 let mut count = 0;
1964 let directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)];
1965
1966 for (df, dr) in directions {
1967 count += self.count_sliding_moves(board, square, color, df, dr);
1968 }
1969
1970 count
1971 }
1972
1973 fn count_rook_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1975 let mut count = 0;
1976 let directions = [(1, 0), (-1, 0), (0, 1), (0, -1)];
1977
1978 for (df, dr) in directions {
1979 count += self.count_sliding_moves(board, square, color, df, dr);
1980 }
1981
1982 count
1983 }
1984
1985 fn count_queen_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1987 let mut count = 0;
1988 let directions = [
1989 (1, 0),
1990 (-1, 0),
1991 (0, 1),
1992 (0, -1), (1, 1),
1994 (1, -1),
1995 (-1, 1),
1996 (-1, -1), ];
1998
1999 for (df, dr) in directions {
2000 count += self.count_sliding_moves(board, square, color, df, dr);
2001 }
2002
2003 count
2004 }
2005
2006 fn count_sliding_moves(
2008 &self,
2009 board: &Board,
2010 square: Square,
2011 color: Color,
2012 df: i8,
2013 dr: i8,
2014 ) -> usize {
2015 let mut count = 0;
2016 let mut file = square.get_file().to_index() as i8;
2017 let mut rank = square.get_rank().to_index() as i8;
2018
2019 loop {
2020 file += df;
2021 rank += dr;
2022
2023 if !(0..8).contains(&file) || !(0..8).contains(&rank) {
2024 break;
2025 }
2026
2027 let dest_square = Square::make_square(
2028 chess::Rank::from_index(rank as usize),
2029 chess::File::from_index(file as usize),
2030 );
2031 if let Some(_piece_on_dest) = board.piece_on(dest_square) {
2032 if board.color_on(dest_square) != Some(color) {
2033 count += 1; }
2035 break; } else {
2037 count += 1; }
2039 }
2040
2041 count
2042 }
2043
2044 fn calculate_pawn_mobility(&self, board: &Board, color: Color) -> f32 {
2046 let mut mobility = 0.0;
2047 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2048
2049 let direction = if color == Color::White { 1 } else { -1 };
2050
2051 for pawn_square in pawns {
2052 let file = pawn_square.get_file().to_index() as i8;
2053 let rank = pawn_square.get_rank().to_index() as i8;
2054
2055 let advance_rank = rank + direction;
2057 if (0..8).contains(&advance_rank) {
2058 let advance_square = Square::make_square(
2059 chess::Rank::from_index(advance_rank as usize),
2060 pawn_square.get_file(),
2061 );
2062 if board.piece_on(advance_square).is_none() {
2063 mobility += 1.0; let starting_rank = if color == Color::White { 1 } else { 6 };
2067 if rank == starting_rank {
2068 let double_advance_rank = advance_rank + direction;
2069 let double_advance_square = Square::make_square(
2070 chess::Rank::from_index(double_advance_rank as usize),
2071 pawn_square.get_file(),
2072 );
2073 if board.piece_on(double_advance_square).is_none() {
2074 mobility += 0.5; }
2076 }
2077 }
2078 }
2079
2080 for capture_file in [file - 1, file + 1] {
2082 if (0..8).contains(&capture_file) && (0..8).contains(&advance_rank) {
2083 let capture_square = Square::make_square(
2084 chess::Rank::from_index(advance_rank as usize),
2085 chess::File::from_index(capture_file as usize),
2086 );
2087 if let Some(_piece) = board.piece_on(capture_square) {
2088 if board.color_on(capture_square) != Some(color) {
2089 mobility += 2.0; }
2091 }
2092 }
2093 }
2094 }
2095
2096 mobility
2097 }
2098
2099 fn tactical_bonuses(&self, board: &Board) -> f32 {
2101 let mut bonus = 0.0;
2102
2103 bonus += self.mobility_evaluation(board);
2105
2106 let captures = self.generate_captures(board);
2108 let capture_bonus = captures.len() as f32 * 0.1;
2109
2110 bonus += self.center_control_evaluation(board);
2112
2113 if board.side_to_move() == Color::White {
2115 bonus += capture_bonus;
2116 } else {
2117 bonus -= capture_bonus;
2118 }
2119
2120 bonus
2121 }
2122
2123 fn center_control_evaluation(&self, board: &Board) -> f32 {
2125 let mut score = 0.0;
2126 let center_squares = [
2127 Square::make_square(chess::Rank::Fourth, chess::File::D),
2128 Square::make_square(chess::Rank::Fourth, chess::File::E),
2129 Square::make_square(chess::Rank::Fifth, chess::File::D),
2130 Square::make_square(chess::Rank::Fifth, chess::File::E),
2131 ];
2132
2133 let extended_center = [
2134 Square::make_square(chess::Rank::Third, chess::File::C),
2135 Square::make_square(chess::Rank::Third, chess::File::D),
2136 Square::make_square(chess::Rank::Third, chess::File::E),
2137 Square::make_square(chess::Rank::Third, chess::File::F),
2138 Square::make_square(chess::Rank::Fourth, chess::File::C),
2139 Square::make_square(chess::Rank::Fourth, chess::File::F),
2140 Square::make_square(chess::Rank::Fifth, chess::File::C),
2141 Square::make_square(chess::Rank::Fifth, chess::File::F),
2142 Square::make_square(chess::Rank::Sixth, chess::File::C),
2143 Square::make_square(chess::Rank::Sixth, chess::File::D),
2144 Square::make_square(chess::Rank::Sixth, chess::File::E),
2145 Square::make_square(chess::Rank::Sixth, chess::File::F),
2146 ];
2147
2148 for &square in ¢er_squares {
2150 if let Some(piece) = board.piece_on(square) {
2151 if piece == chess::Piece::Pawn {
2152 if let Some(color) = board.color_on(square) {
2153 let bonus = if color == Color::White { 30.0 } else { -30.0 };
2154 score += bonus;
2155 }
2156 }
2157 }
2158 }
2159
2160 for &square in &extended_center {
2162 if let Some(_piece) = board.piece_on(square) {
2163 if let Some(color) = board.color_on(square) {
2164 let bonus = if color == Color::White { 5.0 } else { -5.0 };
2165 score += bonus;
2166 }
2167 }
2168 }
2169
2170 score
2171 }
2172
2173 fn king_safety(&self, board: &Board) -> f32 {
2175 let mut safety = 0.0;
2176 let game_phase = self.detect_game_phase(board);
2177
2178 for color in [Color::White, Color::Black] {
2179 let mut king_safety = 0.0;
2180 let king_square = board.king_square(color);
2181 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
2182
2183 king_safety += self.evaluate_castling_safety(board, color, king_square, game_phase);
2185
2186 king_safety += self.evaluate_pawn_shield(board, color, king_square, game_phase);
2188
2189 king_safety += self.evaluate_king_attackers(board, color, king_square);
2191
2192 king_safety += self.evaluate_open_lines_near_king(board, color, king_square);
2194
2195 if game_phase == GamePhase::Endgame {
2197 king_safety += self.evaluate_king_endgame_activity(board, color, king_square);
2198 }
2199
2200 king_safety += self.evaluate_king_zone_control(board, color, king_square);
2202
2203 if board.checkers().popcnt() > 0 && board.side_to_move() == color {
2205 let check_severity = self.evaluate_check_severity(board, color);
2206 king_safety -= check_severity;
2207 }
2208
2209 safety += king_safety * multiplier;
2210 }
2211
2212 safety
2213 }
2214
2215 fn evaluate_castling_safety(
2217 &self,
2218 board: &Board,
2219 color: Color,
2220 king_square: Square,
2221 game_phase: GamePhase,
2222 ) -> f32 {
2223 let mut score = 0.0;
2224
2225 let starting_square = if color == Color::White {
2226 Square::E1
2227 } else {
2228 Square::E8
2229 };
2230 let kingside_castle = if color == Color::White {
2231 Square::G1
2232 } else {
2233 Square::G8
2234 };
2235 let queenside_castle = if color == Color::White {
2236 Square::C1
2237 } else {
2238 Square::C8
2239 };
2240
2241 match game_phase {
2242 GamePhase::Opening | GamePhase::Middlegame => {
2243 if king_square == kingside_castle {
2244 score += 50.0; } else if king_square == queenside_castle {
2246 score += 35.0; } else if king_square == starting_square {
2248 let castle_rights = board.castle_rights(color);
2250 if castle_rights.has_kingside() {
2251 score += 25.0;
2252 }
2253 if castle_rights.has_queenside() {
2254 score += 15.0;
2255 }
2256 } else {
2257 score -= 80.0;
2259 }
2260 }
2261 GamePhase::Endgame => {
2262 let rank = king_square.get_rank().to_index() as i8;
2264 let file = king_square.get_file().to_index() as i8;
2265 let center_distance = (rank as f32 - 3.5).abs() + (file as f32 - 3.5).abs();
2266 score += (7.0 - center_distance) * 5.0; }
2268 }
2269
2270 score
2271 }
2272
2273 fn evaluate_pawn_shield(
2275 &self,
2276 board: &Board,
2277 color: Color,
2278 king_square: Square,
2279 game_phase: GamePhase,
2280 ) -> f32 {
2281 if game_phase == GamePhase::Endgame {
2282 return 0.0; }
2284
2285 let mut shield_score = 0.0;
2286 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2287 let king_file = king_square.get_file().to_index() as i8;
2288 let king_rank = king_square.get_rank().to_index() as i8;
2289
2290 let shield_files = [king_file - 1, king_file, king_file + 1];
2292 let forward_direction = if color == Color::White { 1 } else { -1 };
2293
2294 for &file in &shield_files {
2295 if (0..8).contains(&file) {
2296 let mut found_pawn = false;
2297 let file_mask = self.get_file_mask(chess::File::from_index(file as usize));
2298 let file_pawns = pawns & file_mask;
2299
2300 for pawn_square in file_pawns {
2301 let pawn_rank = pawn_square.get_rank().to_index() as i8;
2302 let rank_distance = (pawn_rank - king_rank) * forward_direction;
2303
2304 if rank_distance > 0 && rank_distance <= 3 {
2305 found_pawn = true;
2306 let protection_value = match rank_distance {
2308 1 => 25.0, 2 => 15.0, 3 => 8.0, _ => 0.0,
2312 };
2313 shield_score += protection_value;
2314 break;
2315 }
2316 }
2317
2318 if !found_pawn {
2320 shield_score -= 20.0;
2321 }
2322 }
2323 }
2324
2325 let is_kingside = king_file >= 6;
2327 let is_queenside = king_file <= 2;
2328
2329 if is_kingside {
2330 shield_score += self.evaluate_kingside_pawn_structure(board, color);
2331 } else if is_queenside {
2332 shield_score += self.evaluate_queenside_pawn_structure(board, color);
2333 }
2334
2335 shield_score
2336 }
2337
2338 fn evaluate_kingside_pawn_structure(&self, board: &Board, color: Color) -> f32 {
2340 let mut score = 0.0;
2341 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2342 let base_rank = if color == Color::White { 1 } else { 6 };
2343
2344 for (file_idx, ideal_rank) in [(5, base_rank), (6, base_rank), (7, base_rank)] {
2346 let file_mask = self.get_file_mask(chess::File::from_index(file_idx));
2347 let file_pawns = pawns & file_mask;
2348
2349 let mut found_intact = false;
2350 for pawn_square in file_pawns {
2351 if pawn_square.get_rank().to_index() == ideal_rank {
2352 found_intact = true;
2353 score += 10.0; break;
2355 }
2356 }
2357
2358 if !found_intact {
2359 score -= 15.0; }
2361 }
2362
2363 score
2364 }
2365
2366 fn evaluate_queenside_pawn_structure(&self, board: &Board, color: Color) -> f32 {
2368 let mut score = 0.0;
2369 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2370 let base_rank = if color == Color::White { 1 } else { 6 };
2371
2372 for (file_idx, ideal_rank) in [(0, base_rank), (1, base_rank), (2, base_rank)] {
2374 let file_mask = self.get_file_mask(chess::File::from_index(file_idx));
2375 let file_pawns = pawns & file_mask;
2376
2377 let mut found_intact = false;
2378 for pawn_square in file_pawns {
2379 if pawn_square.get_rank().to_index() == ideal_rank {
2380 found_intact = true;
2381 score += 8.0; break;
2383 }
2384 }
2385
2386 if !found_intact {
2387 score -= 12.0; }
2389 }
2390
2391 score
2392 }
2393
2394 fn evaluate_king_attackers(&self, board: &Board, color: Color, king_square: Square) -> f32 {
2396 let mut attack_score = 0.0;
2397 let enemy_color = !color;
2398
2399 let enemy_queens = board.pieces(chess::Piece::Queen) & board.color_combined(enemy_color);
2401 let enemy_rooks = board.pieces(chess::Piece::Rook) & board.color_combined(enemy_color);
2402 let enemy_bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(enemy_color);
2403 let enemy_knights = board.pieces(chess::Piece::Knight) & board.color_combined(enemy_color);
2404
2405 for queen_square in enemy_queens {
2407 if self.can_attack_square(board, queen_square, king_square, chess::Piece::Queen) {
2408 attack_score -= 50.0;
2409 }
2410 }
2411
2412 for rook_square in enemy_rooks {
2414 if self.can_attack_square(board, rook_square, king_square, chess::Piece::Rook) {
2415 attack_score -= 30.0;
2416 }
2417 }
2418
2419 for bishop_square in enemy_bishops {
2421 if self.can_attack_square(board, bishop_square, king_square, chess::Piece::Bishop) {
2422 attack_score -= 25.0;
2423 }
2424 }
2425
2426 for knight_square in enemy_knights {
2428 if self.can_attack_square(board, knight_square, king_square, chess::Piece::Knight) {
2429 attack_score -= 20.0;
2430 }
2431 }
2432
2433 attack_score
2434 }
2435
2436 fn can_attack_square(
2438 &self,
2439 board: &Board,
2440 piece_square: Square,
2441 target_square: Square,
2442 piece_type: chess::Piece,
2443 ) -> bool {
2444 match piece_type {
2445 chess::Piece::Queen | chess::Piece::Rook | chess::Piece::Bishop => {
2446 self.has_clear_line_of_attack(board, piece_square, target_square, piece_type)
2448 }
2449 chess::Piece::Knight => {
2450 let file_diff = (piece_square.get_file().to_index() as i8
2452 - target_square.get_file().to_index() as i8)
2453 .abs();
2454 let rank_diff = (piece_square.get_rank().to_index() as i8
2455 - target_square.get_rank().to_index() as i8)
2456 .abs();
2457 (file_diff == 2 && rank_diff == 1) || (file_diff == 1 && rank_diff == 2)
2458 }
2459 _ => false,
2460 }
2461 }
2462
2463 fn has_clear_line_of_attack(
2465 &self,
2466 board: &Board,
2467 from: Square,
2468 to: Square,
2469 piece_type: chess::Piece,
2470 ) -> bool {
2471 let from_file = from.get_file().to_index() as i8;
2472 let from_rank = from.get_rank().to_index() as i8;
2473 let to_file = to.get_file().to_index() as i8;
2474 let to_rank = to.get_rank().to_index() as i8;
2475
2476 let file_diff = to_file - from_file;
2477 let rank_diff = to_rank - from_rank;
2478
2479 let is_valid_attack = match piece_type {
2481 chess::Piece::Rook | chess::Piece::Queen => {
2482 file_diff == 0 || rank_diff == 0 || file_diff.abs() == rank_diff.abs()
2483 }
2484 chess::Piece::Bishop => file_diff.abs() == rank_diff.abs(),
2485 _ => false,
2486 };
2487
2488 if !is_valid_attack {
2489 return false;
2490 }
2491
2492 let file_step = if file_diff == 0 {
2494 0
2495 } else {
2496 file_diff.signum()
2497 };
2498 let rank_step = if rank_diff == 0 {
2499 0
2500 } else {
2501 rank_diff.signum()
2502 };
2503
2504 let mut current_file = from_file + file_step;
2505 let mut current_rank = from_rank + rank_step;
2506
2507 while current_file != to_file || current_rank != to_rank {
2508 let square = Square::make_square(
2509 chess::Rank::from_index(current_rank as usize),
2510 chess::File::from_index(current_file as usize),
2511 );
2512 if board.piece_on(square).is_some() {
2513 return false; }
2515 current_file += file_step;
2516 current_rank += rank_step;
2517 }
2518
2519 true
2520 }
2521
2522 fn evaluate_open_lines_near_king(
2524 &self,
2525 board: &Board,
2526 color: Color,
2527 king_square: Square,
2528 ) -> f32 {
2529 let mut line_score = 0.0;
2530 let king_file = king_square.get_file();
2531 let _king_rank = king_square.get_rank();
2532
2533 for file_offset in -1..=1i8 {
2535 let file_index = (king_file.to_index() as i8 + file_offset).clamp(0, 7) as usize;
2536 let file = chess::File::from_index(file_index);
2537 if self.is_open_file(board, file) {
2538 line_score -= 20.0; } else if self.is_semi_open_file(board, file, color) {
2540 line_score -= 10.0; }
2542 }
2543
2544 line_score += self.evaluate_diagonal_safety(board, color, king_square);
2546
2547 line_score
2548 }
2549
2550 fn is_open_file(&self, board: &Board, file: chess::File) -> bool {
2552 let file_mask = self.get_file_mask(file);
2553 let all_pawns = board.pieces(chess::Piece::Pawn);
2554 (all_pawns & file_mask).popcnt() == 0
2555 }
2556
2557 fn is_semi_open_file(&self, board: &Board, file: chess::File, color: Color) -> bool {
2559 let file_mask = self.get_file_mask(file);
2560 let own_pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2561 (own_pawns & file_mask).popcnt() == 0
2562 }
2563
2564 fn evaluate_diagonal_safety(&self, board: &Board, color: Color, king_square: Square) -> f32 {
2566 let mut score = 0.0;
2567 let enemy_color = !color;
2568 let enemy_bishops_queens = (board.pieces(chess::Piece::Bishop)
2569 | board.pieces(chess::Piece::Queen))
2570 & board.color_combined(enemy_color);
2571
2572 let directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)];
2574
2575 for (file_dir, rank_dir) in directions {
2576 if self.has_diagonal_threat(
2577 board,
2578 king_square,
2579 file_dir,
2580 rank_dir,
2581 enemy_bishops_queens,
2582 ) {
2583 score -= 15.0; }
2585 }
2586
2587 score
2588 }
2589
2590 fn has_diagonal_threat(
2592 &self,
2593 board: &Board,
2594 king_square: Square,
2595 file_dir: i8,
2596 rank_dir: i8,
2597 enemy_pieces: chess::BitBoard,
2598 ) -> bool {
2599 let mut file = king_square.get_file().to_index() as i8 + file_dir;
2600 let mut rank = king_square.get_rank().to_index() as i8 + rank_dir;
2601
2602 while (0..8).contains(&file) && (0..8).contains(&rank) {
2603 let square = Square::make_square(
2604 chess::Rank::from_index(rank as usize),
2605 chess::File::from_index(file as usize),
2606 );
2607 if let Some(_piece) = board.piece_on(square) {
2608 return (enemy_pieces & chess::BitBoard::from_square(square)).popcnt() > 0;
2610 }
2611 file += file_dir;
2612 rank += rank_dir;
2613 }
2614
2615 false
2616 }
2617
2618 fn evaluate_king_endgame_activity(
2620 &self,
2621 board: &Board,
2622 color: Color,
2623 king_square: Square,
2624 ) -> f32 {
2625 let mut activity_score = 0.0;
2626
2627 let file = king_square.get_file().to_index() as f32;
2629 let rank = king_square.get_rank().to_index() as f32;
2630 let center_distance = ((file - 3.5).abs() + (rank - 3.5).abs()) / 2.0;
2631 activity_score += (3.5 - center_distance) * 10.0;
2632
2633 let enemy_pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(!color);
2635 for enemy_pawn in enemy_pawns {
2636 let distance = ((king_square.get_file().to_index() as i8
2637 - enemy_pawn.get_file().to_index() as i8)
2638 .abs()
2639 + (king_square.get_rank().to_index() as i8
2640 - enemy_pawn.get_rank().to_index() as i8)
2641 .abs()) as f32;
2642 if distance <= 3.0 {
2643 activity_score += 5.0; }
2645 }
2646
2647 activity_score
2648 }
2649
2650 fn evaluate_king_zone_control(&self, board: &Board, color: Color, king_square: Square) -> f32 {
2652 let mut control_score = 0.0;
2653 let king_file = king_square.get_file().to_index() as i8;
2654 let king_rank = king_square.get_rank().to_index() as i8;
2655
2656 for file_offset in -1..=1 {
2658 for rank_offset in -1..=1 {
2659 if file_offset == 0 && rank_offset == 0 {
2660 continue; }
2662
2663 let check_file = king_file + file_offset;
2664 let check_rank = king_rank + rank_offset;
2665
2666 if (0..8).contains(&check_file) && (0..8).contains(&check_rank) {
2667 let square = Square::make_square(
2668 chess::Rank::from_index(check_rank as usize),
2669 chess::File::from_index(check_file as usize),
2670 );
2671 if let Some(_piece) = board.piece_on(square) {
2672 if board.color_on(square) == Some(color) {
2673 control_score += 3.0; } else {
2675 control_score -= 5.0; }
2677 }
2678 }
2679 }
2680 }
2681
2682 control_score
2683 }
2684
2685 fn evaluate_check_severity(&self, board: &Board, _color: Color) -> f32 {
2687 let checkers = board.checkers();
2688 let check_count = checkers.popcnt();
2689
2690 let base_penalty = match check_count {
2691 0 => 0.0,
2692 1 => 50.0, 2 => 150.0, _ => 200.0, };
2696
2697 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
2699 let king_moves = legal_moves
2700 .iter()
2701 .filter(|mv| board.piece_on(mv.get_source()) == Some(chess::Piece::King))
2702 .count();
2703
2704 let escape_penalty = match king_moves {
2705 0 => 100.0, 1 => 30.0, 2 => 15.0, _ => 0.0, };
2710
2711 base_penalty + escape_penalty
2712 }
2713
2714 fn determine_game_phase(&self, board: &Board) -> GamePhase {
2716 let mut material_count = 0;
2718
2719 for piece in [
2720 chess::Piece::Queen,
2721 chess::Piece::Rook,
2722 chess::Piece::Bishop,
2723 chess::Piece::Knight,
2724 ] {
2725 material_count += board.pieces(piece).popcnt();
2726 }
2727
2728 match material_count {
2729 0..=4 => GamePhase::Endgame, 5..=12 => GamePhase::Middlegame, _ => GamePhase::Opening, }
2733 }
2734
2735 #[allow(dead_code)]
2737 fn count_king_attackers(&self, board: &Board, color: Color) -> u32 {
2738 let king_square = board.king_square(color);
2739 let opponent_color = if color == Color::White {
2740 Color::Black
2741 } else {
2742 Color::White
2743 };
2744
2745 let mut attackers = 0;
2747
2748 for piece in [
2750 chess::Piece::Queen,
2751 chess::Piece::Rook,
2752 chess::Piece::Bishop,
2753 chess::Piece::Knight,
2754 chess::Piece::Pawn,
2755 ] {
2756 let enemy_pieces = board.pieces(piece) & board.color_combined(opponent_color);
2757
2758 for square in enemy_pieces {
2760 let rank_diff = (king_square.get_rank().to_index() as i32
2761 - square.get_rank().to_index() as i32)
2762 .abs();
2763 let file_diff = (king_square.get_file().to_index() as i32
2764 - square.get_file().to_index() as i32)
2765 .abs();
2766
2767 let is_threat = match piece {
2769 chess::Piece::Queen => rank_diff <= 2 || file_diff <= 2,
2770 chess::Piece::Rook => rank_diff <= 2 || file_diff <= 2,
2771 chess::Piece::Bishop => rank_diff == file_diff && rank_diff <= 2,
2772 chess::Piece::Knight => {
2773 (rank_diff == 2 && file_diff == 1) || (rank_diff == 1 && file_diff == 2)
2774 }
2775 chess::Piece::Pawn => {
2776 rank_diff == 1
2777 && file_diff == 1
2778 && ((color == Color::White
2779 && square.get_rank().to_index()
2780 > king_square.get_rank().to_index())
2781 || (color == Color::Black
2782 && square.get_rank().to_index()
2783 < king_square.get_rank().to_index()))
2784 }
2785 _ => false,
2786 };
2787
2788 if is_threat {
2789 attackers += 1;
2790 }
2791 }
2792 }
2793
2794 attackers
2795 }
2796
2797 fn get_file_mask(&self, file: chess::File) -> chess::BitBoard {
2799 chess::BitBoard(0x0101010101010101u64 << file.to_index())
2800 }
2801
2802 fn evaluate_pawn_structure(&self, board: &Board) -> f32 {
2804 let mut score = 0.0;
2805
2806 for color in [Color::White, Color::Black] {
2807 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
2808 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2809
2810 for file in 0..8 {
2812 let file_mask = self.get_file_mask(chess::File::from_index(file));
2813 let file_pawns = pawns & file_mask;
2814 let pawn_count = file_pawns.popcnt();
2815
2816 if pawn_count > 1 {
2818 score += -0.5 * multiplier * (pawn_count - 1) as f32; }
2820
2821 if pawn_count > 0 {
2823 let has_adjacent_pawns = self.has_adjacent_pawns(board, color, file);
2824 if !has_adjacent_pawns {
2825 score += -0.3 * multiplier; }
2827 }
2828
2829 for square in file_pawns {
2831 if self.is_passed_pawn(board, square, color) {
2833 let rank = square.get_rank().to_index();
2834 let advancement = if color == Color::White {
2835 rank
2836 } else {
2837 7 - rank
2838 };
2839 score += (0.2 + advancement as f32 * 0.3) * multiplier; }
2841
2842 if self.is_backward_pawn(board, square, color) {
2844 score += -0.2 * multiplier;
2845 }
2846
2847 if self.has_pawn_support(board, square, color) {
2849 score += 0.1 * multiplier;
2850 }
2851 }
2852 }
2853
2854 score += self.evaluate_pawn_chains(board, color) * multiplier;
2856 }
2857
2858 score
2859 }
2860
2861 fn has_adjacent_pawns(&self, board: &Board, color: Color, file: usize) -> bool {
2863 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2864
2865 if file > 0 {
2867 let left_file_mask = self.get_file_mask(chess::File::from_index(file - 1));
2868 if (pawns & left_file_mask).popcnt() > 0 {
2869 return true;
2870 }
2871 }
2872
2873 if file < 7 {
2874 let right_file_mask = self.get_file_mask(chess::File::from_index(file + 1));
2875 if (pawns & right_file_mask).popcnt() > 0 {
2876 return true;
2877 }
2878 }
2879
2880 false
2881 }
2882
2883 fn is_passed_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
2885 let opponent_color = if color == Color::White {
2886 Color::Black
2887 } else {
2888 Color::White
2889 };
2890 let opponent_pawns =
2891 board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color);
2892
2893 let file = pawn_square.get_file().to_index();
2894 let rank = pawn_square.get_rank().to_index();
2895
2896 for opponent_square in opponent_pawns {
2898 let opp_file = opponent_square.get_file().to_index();
2899 let opp_rank = opponent_square.get_rank().to_index();
2900
2901 let file_diff = (file as i32 - opp_file as i32).abs();
2903
2904 if file_diff <= 1 {
2905 if color == Color::White && opp_rank > rank {
2907 return false; }
2909 if color == Color::Black && opp_rank < rank {
2910 return false; }
2912 }
2913 }
2914
2915 true
2916 }
2917
2918 fn is_backward_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
2920 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2921 let file = pawn_square.get_file().to_index();
2922 let rank = pawn_square.get_rank().to_index();
2923
2924 for support_file in [file.saturating_sub(1), (file + 1).min(7)] {
2926 if support_file == file {
2927 continue;
2928 }
2929
2930 let file_mask = self.get_file_mask(chess::File::from_index(support_file));
2931 let file_pawns = pawns & file_mask;
2932
2933 for support_square in file_pawns {
2934 let support_rank = support_square.get_rank().to_index();
2935
2936 if color == Color::White && support_rank < rank {
2938 return false; }
2940 if color == Color::Black && support_rank > rank {
2941 return false; }
2943 }
2944 }
2945
2946 true
2947 }
2948
2949 fn has_pawn_support(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
2951 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2952 let file = pawn_square.get_file().to_index();
2953 let rank = pawn_square.get_rank().to_index();
2954
2955 for support_file in [file.saturating_sub(1), (file + 1).min(7)] {
2957 if support_file == file {
2958 continue;
2959 }
2960
2961 let file_mask = self.get_file_mask(chess::File::from_index(support_file));
2962 let file_pawns = pawns & file_mask;
2963
2964 for support_square in file_pawns {
2965 let support_rank = support_square.get_rank().to_index();
2966
2967 if (support_rank as i32 - rank as i32).abs() == 1 {
2969 return true;
2970 }
2971 }
2972 }
2973
2974 false
2975 }
2976
2977 fn evaluate_pawn_chains(&self, board: &Board, color: Color) -> f32 {
2979 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2980 let mut chain_score = 0.0;
2981
2982 let mut chain_lengths = Vec::new();
2984 let mut visited = std::collections::HashSet::new();
2985
2986 for pawn_square in pawns {
2987 if visited.contains(&pawn_square) {
2988 continue;
2989 }
2990
2991 let chain_length = self.count_pawn_chain(board, pawn_square, color, &mut visited);
2992 if chain_length > 1 {
2993 chain_lengths.push(chain_length);
2994 }
2995 }
2996
2997 for &length in &chain_lengths {
2999 chain_score += (length as f32 - 1.0) * 0.15; }
3001
3002 chain_score
3003 }
3004
3005 #[allow(clippy::only_used_in_recursion)]
3007 fn count_pawn_chain(
3008 &self,
3009 board: &Board,
3010 start_square: Square,
3011 color: Color,
3012 visited: &mut std::collections::HashSet<Square>,
3013 ) -> usize {
3014 if visited.contains(&start_square) {
3015 return 0;
3016 }
3017
3018 visited.insert(start_square);
3019 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3020
3021 if (pawns & chess::BitBoard::from_square(start_square)) == chess::BitBoard(0) {
3023 return 0;
3024 }
3025
3026 let mut count = 1;
3027 let file = start_square.get_file().to_index();
3028 let rank = start_square.get_rank().to_index();
3029
3030 for &(file_offset, rank_offset) in &[(-1i32, -1i32), (-1, 1), (1, -1), (1, 1)] {
3032 let new_file = file as i32 + file_offset;
3033 let new_rank = rank as i32 + rank_offset;
3034
3035 if (0..8).contains(&new_file) && (0..8).contains(&new_rank) {
3036 let square_index = (new_rank * 8 + new_file) as u8;
3037 let new_square = unsafe { Square::new(square_index) };
3038 if (pawns & chess::BitBoard::from_square(new_square)) != chess::BitBoard(0)
3039 && !visited.contains(&new_square)
3040 {
3041 count += self.count_pawn_chain(board, new_square, color, visited);
3042 }
3043 }
3044 }
3045
3046 count
3047 }
3048
3049 fn is_tactical_position(&self, board: &Board) -> bool {
3051 if board.checkers().popcnt() > 0 {
3053 return true;
3054 }
3055
3056 let captures = self.generate_captures(board);
3058 if !captures.is_empty() {
3059 return true;
3060 }
3061
3062 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
3064 if legal_moves.len() > 35 {
3065 return true;
3066 }
3067
3068 false
3069 }
3070
3071 fn is_capture_or_promotion(&self, chess_move: &ChessMove, board: &Board) -> bool {
3073 board.piece_on(chess_move.get_dest()).is_some() || chess_move.get_promotion().is_some()
3074 }
3075
3076 fn has_non_pawn_material(&self, board: &Board, color: Color) -> bool {
3078 let pieces = board.color_combined(color)
3079 & !board.pieces(chess::Piece::Pawn)
3080 & !board.pieces(chess::Piece::King);
3081 pieces.popcnt() > 0
3082 }
3083
3084 fn is_killer_move(&self, chess_move: &ChessMove) -> bool {
3086 for depth_killers in &self.killer_moves {
3088 for killer_move in depth_killers.iter().flatten() {
3089 if killer_move == chess_move {
3090 return true;
3091 }
3092 }
3093 }
3094 false
3095 }
3096
3097 fn store_killer_move(&mut self, chess_move: ChessMove, depth: u32) {
3099 let depth_idx = (depth as usize).min(self.killer_moves.len() - 1);
3100
3101 if let Some(first_killer) = self.killer_moves[depth_idx][0] {
3103 if first_killer != chess_move {
3104 self.killer_moves[depth_idx][1] = Some(first_killer);
3105 self.killer_moves[depth_idx][0] = Some(chess_move);
3106 }
3107 } else {
3108 self.killer_moves[depth_idx][0] = Some(chess_move);
3109 }
3110 }
3111
3112 fn update_history(&mut self, chess_move: &ChessMove, depth: u32) {
3114 let key = (chess_move.get_source(), chess_move.get_dest());
3115 let bonus = depth * depth; let current = self.history_heuristic.get(&key).unwrap_or(&0);
3118 self.history_heuristic.insert(key, current + bonus);
3119 }
3120
3121 fn get_history_score(&self, chess_move: &ChessMove) -> u32 {
3123 let key = (chess_move.get_source(), chess_move.get_dest());
3124 *self.history_heuristic.get(&key).unwrap_or(&0)
3125 }
3126
3127 #[allow(dead_code)]
3129 fn store_counter_move(&mut self, refutation: ChessMove) {
3130 if let Some(last_move) = self.last_move {
3131 let last_move_key = (last_move.get_source(), last_move.get_dest());
3132 self.counter_moves.insert(last_move_key, refutation);
3133 }
3134 }
3135
3136 #[allow(dead_code)]
3138 fn update_last_move(&mut self, chess_move: ChessMove) {
3139 self.last_move = Some(chess_move);
3140 }
3141
3142 pub fn clear_cache(&mut self) {
3144 self.transposition_table.clear();
3145 }
3146
3147 pub fn get_stats(&self) -> (u64, usize) {
3149 (self.nodes_searched, self.transposition_table.len())
3150 }
3151
3152 fn evaluate_endgame_patterns(&self, board: &Board) -> f32 {
3154 let mut score = 0.0;
3155
3156 let piece_count = self.count_all_pieces(board);
3158 if piece_count > 10 {
3159 return 0.0; }
3161
3162 let endgame_weight = self.config.endgame_evaluation_weight;
3164
3165 score += self.evaluate_king_pawn_endgames(board) * endgame_weight;
3167 score += self.evaluate_basic_mate_patterns(board) * endgame_weight;
3168 score += self.evaluate_opposition_patterns(board) * endgame_weight;
3169 score += self.evaluate_key_squares(board) * endgame_weight;
3170 score += self.evaluate_zugzwang_patterns(board) * endgame_weight;
3171
3172 score += self.evaluate_piece_coordination_endgame(board) * endgame_weight;
3174 score += self.evaluate_fortress_patterns(board) * endgame_weight;
3175 score += self.evaluate_theoretical_endgames(board) * endgame_weight;
3176
3177 score
3178 }
3179
3180 fn count_all_pieces(&self, board: &Board) -> u32 {
3182 let mut count = 0;
3183 for piece in [
3184 chess::Piece::Pawn,
3185 chess::Piece::Knight,
3186 chess::Piece::Bishop,
3187 chess::Piece::Rook,
3188 chess::Piece::Queen,
3189 ] {
3190 count += board.pieces(piece).popcnt();
3191 }
3192 count += board.pieces(chess::Piece::King).popcnt(); count
3194 }
3195
3196 fn evaluate_king_pawn_endgames(&self, board: &Board) -> f32 {
3198 let mut score = 0.0;
3199
3200 for color in [Color::White, Color::Black] {
3202 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3203 let king_square = board.king_square(color);
3204 let opponent_king_square = board.king_square(!color);
3205 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3206
3207 for pawn_square in pawns {
3208 if self.is_passed_pawn(board, pawn_square, color) {
3209 let _pawn_file = pawn_square.get_file().to_index();
3210 let pawn_rank = pawn_square.get_rank().to_index();
3211
3212 let promotion_rank = if color == Color::White { 7 } else { 0 };
3214 let promotion_square = Square::make_square(
3215 chess::Rank::from_index(promotion_rank),
3216 chess::File::from_index(_pawn_file),
3217 );
3218
3219 let king_distance = self.square_distance(king_square, promotion_square);
3221 let opponent_king_distance =
3222 self.square_distance(opponent_king_square, promotion_square);
3223 let pawn_distance = (promotion_rank as i32 - pawn_rank as i32).unsigned_abs();
3224
3225 if pawn_distance < opponent_king_distance {
3227 score += 2.0 * multiplier; } else if king_distance < opponent_king_distance {
3229 score += 1.0 * multiplier; }
3231 }
3232 }
3233 }
3234
3235 score
3236 }
3237
3238 fn evaluate_basic_mate_patterns(&self, board: &Board) -> f32 {
3240 let mut score = 0.0;
3241
3242 for color in [Color::White, Color::Black] {
3243 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3244 let opponent_color = !color;
3245
3246 let queens = (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt();
3247 let rooks = (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt();
3248 let bishops =
3249 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt();
3250 let knights =
3251 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt();
3252
3253 let opp_queens =
3254 (board.pieces(chess::Piece::Queen) & board.color_combined(opponent_color)).popcnt();
3255 let opp_rooks =
3256 (board.pieces(chess::Piece::Rook) & board.color_combined(opponent_color)).popcnt();
3257 let opp_bishops = (board.pieces(chess::Piece::Bishop)
3258 & board.color_combined(opponent_color))
3259 .popcnt();
3260 let opp_knights = (board.pieces(chess::Piece::Knight)
3261 & board.color_combined(opponent_color))
3262 .popcnt();
3263 let opp_pawns =
3264 (board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color)).popcnt();
3265
3266 if opp_queens == 0
3268 && opp_rooks == 0
3269 && opp_bishops == 0
3270 && opp_knights == 0
3271 && opp_pawns == 0
3272 {
3273 if queens > 0 || rooks > 0 {
3275 let king_square = board.king_square(color);
3277 let opponent_king_square = board.king_square(opponent_color);
3278 let corner_distance = self.distance_to_nearest_corner(opponent_king_square);
3279 let king_distance = self.square_distance(king_square, opponent_king_square);
3280
3281 score += 1.0 * multiplier; score += (7.0 - corner_distance as f32) * 0.1 * multiplier; score += (8.0 - king_distance as f32) * 0.05 * multiplier; }
3285
3286 if bishops >= 2 {
3287 let opponent_king_square = board.king_square(opponent_color);
3289 let corner_distance = self.distance_to_nearest_corner(opponent_king_square);
3290 score += 0.8 * multiplier; score += (7.0 - corner_distance as f32) * 0.08 * multiplier;
3292 }
3293
3294 if bishops >= 1 && knights >= 1 {
3295 score += 0.6 * multiplier; }
3298 }
3299 }
3300
3301 score
3302 }
3303
3304 fn evaluate_opposition_patterns(&self, board: &Board) -> f32 {
3306 let mut score = 0.0;
3307
3308 let white_king = board.king_square(Color::White);
3309 let black_king = board.king_square(Color::Black);
3310
3311 let file_diff = (white_king.get_file().to_index() as i32
3312 - black_king.get_file().to_index() as i32)
3313 .abs();
3314 let rank_diff = (white_king.get_rank().to_index() as i32
3315 - black_king.get_rank().to_index() as i32)
3316 .abs();
3317
3318 if (file_diff == 0 && rank_diff == 2) || (file_diff == 2 && rank_diff == 0) {
3320 let opposition_bonus = 0.2;
3322 if board.side_to_move() == Color::White {
3323 score -= opposition_bonus; } else {
3325 score += opposition_bonus; }
3327 }
3328
3329 if file_diff == 0 && rank_diff % 2 == 0 && rank_diff > 2 {
3331 let distant_opposition_bonus = 0.1;
3332 if board.side_to_move() == Color::White {
3333 score -= distant_opposition_bonus;
3334 } else {
3335 score += distant_opposition_bonus;
3336 }
3337 }
3338
3339 score
3340 }
3341
3342 fn evaluate_key_squares(&self, board: &Board) -> f32 {
3344 let mut score = 0.0;
3345
3346 for color in [Color::White, Color::Black] {
3348 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3349 let king_square = board.king_square(color);
3350 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3351
3352 for pawn_square in pawns {
3353 if self.is_passed_pawn(board, pawn_square, color) {
3354 let key_squares = self.get_key_squares(pawn_square, color);
3356
3357 for key_square in key_squares {
3358 let distance = self.square_distance(king_square, key_square);
3359 if distance <= 1 {
3360 score += 0.3 * multiplier; } else if distance <= 2 {
3362 score += 0.1 * multiplier; }
3364 }
3365 }
3366 }
3367 }
3368
3369 score
3370 }
3371
3372 fn evaluate_zugzwang_patterns(&self, board: &Board) -> f32 {
3374 let mut score = 0.0;
3375
3376 let piece_count = self.count_all_pieces(board);
3378 if piece_count <= 6 {
3379 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
3381
3382 if legal_moves.len() <= 3 {
3384 let current_eval = self.quick_evaluate_position(board);
3386 let mut bad_moves = 0;
3387
3388 for chess_move in legal_moves.iter().take(3) {
3389 let new_board = board.make_move_new(*chess_move);
3390 let new_eval = -self.quick_evaluate_position(&new_board); if new_eval < current_eval - 0.5 {
3393 bad_moves += 1;
3394 }
3395 }
3396
3397 if bad_moves >= legal_moves.len() / 2 {
3399 let zugzwang_penalty = 0.3;
3400 if board.side_to_move() == Color::White {
3401 score -= zugzwang_penalty;
3402 } else {
3403 score += zugzwang_penalty;
3404 }
3405 }
3406 }
3407 }
3408
3409 score
3410 }
3411
3412 fn square_distance(&self, sq1: Square, sq2: Square) -> u32 {
3414 let file1 = sq1.get_file().to_index() as i32;
3415 let rank1 = sq1.get_rank().to_index() as i32;
3416 let file2 = sq2.get_file().to_index() as i32;
3417 let rank2 = sq2.get_rank().to_index() as i32;
3418
3419 ((file1 - file2).abs() + (rank1 - rank2).abs()) as u32
3420 }
3421
3422 fn distance_to_nearest_corner(&self, square: Square) -> u32 {
3424 let file = square.get_file().to_index() as i32;
3425 let rank = square.get_rank().to_index() as i32;
3426
3427 let corner_distances = [
3428 file + rank, (7 - file) + rank, file + (7 - rank), (7 - file) + (7 - rank), ];
3433
3434 *corner_distances.iter().min().unwrap() as u32
3435 }
3436
3437 fn get_key_squares(&self, pawn_square: Square, color: Color) -> Vec<Square> {
3439 let mut key_squares = Vec::new();
3440 let file = pawn_square.get_file().to_index();
3441 let rank = pawn_square.get_rank().to_index();
3442
3443 let key_rank = if color == Color::White {
3445 if rank + 2 <= 7 {
3446 rank + 2
3447 } else {
3448 return key_squares;
3449 }
3450 } else if rank >= 2 {
3451 rank - 2
3452 } else {
3453 return key_squares;
3454 };
3455
3456 for key_file in (file.saturating_sub(1))..=(file + 1).min(7) {
3458 let square = Square::make_square(
3459 chess::Rank::from_index(key_rank),
3460 chess::File::from_index(key_file),
3461 );
3462 key_squares.push(square);
3463 }
3464
3465 key_squares
3466 }
3467
3468 fn quick_evaluate_position(&self, board: &Board) -> f32 {
3470 let mut score = 0.0;
3471
3472 score += self.material_balance(board);
3474
3475 for color in [Color::White, Color::Black] {
3477 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3478 let king_square = board.king_square(color);
3479 let file = king_square.get_file().to_index();
3480 let rank = king_square.get_rank().to_index();
3481
3482 let center_distance = (file as f32 - 3.5).abs() + (rank as f32 - 3.5).abs();
3484 score += (7.0 - center_distance) * 0.05 * multiplier;
3485 }
3486
3487 score
3488 }
3489
3490 fn evaluate_piece_coordination_endgame(&self, board: &Board) -> f32 {
3492 let mut score = 0.0;
3493
3494 for color in [Color::White, Color::Black] {
3495 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3496 let king_square = board.king_square(color);
3497
3498 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3500 for rook_square in rooks {
3501 let distance = self.square_distance(king_square, rook_square);
3502 if distance <= 3 {
3503 score += 0.2 * multiplier; }
3505
3506 let rook_rank = rook_square.get_rank().to_index();
3508 if (color == Color::White && rook_rank == 6)
3509 || (color == Color::Black && rook_rank == 1)
3510 {
3511 score += 0.4 * multiplier;
3512 }
3513 }
3514
3515 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
3517 for queen_square in queens {
3518 let distance = self.square_distance(king_square, queen_square);
3519 if distance <= 4 {
3520 score += 0.15 * multiplier; }
3522 }
3523
3524 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3526 if bishops.popcnt() >= 2 {
3527 score += 0.3 * multiplier; }
3529 }
3530
3531 score
3532 }
3533
3534 fn evaluate_fortress_patterns(&self, board: &Board) -> f32 {
3536 let mut score = 0.0;
3537
3538 for color in [Color::White, Color::Black] {
3540 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3541 let opponent_color = !color;
3542
3543 let material_diff = self.calculate_material_difference(board, color);
3545
3546 if material_diff < -2.0 {
3548 let king_square = board.king_square(color);
3550 let king_file = king_square.get_file().to_index();
3551 let king_rank = king_square.get_rank().to_index();
3552
3553 if (king_file <= 1 || king_file >= 6) && (king_rank <= 1 || king_rank >= 6) {
3555 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3556 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3557
3558 if bishops.popcnt() > 0 && pawns.popcnt() >= 2 {
3560 score += 0.5 * multiplier; }
3562 }
3563
3564 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3566 let opp_pawns =
3567 board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color);
3568 if rooks.popcnt() > 0 && opp_pawns.popcnt() >= 3 {
3569 score += 0.3 * multiplier; }
3571 }
3572 }
3573
3574 score
3575 }
3576
3577 fn evaluate_theoretical_endgames(&self, board: &Board) -> f32 {
3579 let mut score = 0.0;
3580
3581 let piece_count = self.count_all_pieces(board);
3582
3583 if piece_count <= 6 {
3585 score += self.evaluate_rook_endgames(board);
3587
3588 score += self.evaluate_bishop_endgames(board);
3590
3591 score += self.evaluate_knight_endgames(board);
3593
3594 score += self.evaluate_mixed_piece_endgames(board);
3596 }
3597
3598 score
3599 }
3600
3601 fn evaluate_rook_endgames(&self, board: &Board) -> f32 {
3603 let mut score = 0.0;
3604
3605 for color in [Color::White, Color::Black] {
3606 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3607 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3608 let opponent_king = board.king_square(!color);
3609
3610 for rook_square in rooks {
3611 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3613 for pawn_square in pawns {
3614 if self.is_passed_pawn(board, pawn_square, color) {
3615 let rook_file = rook_square.get_file().to_index();
3616 let pawn_file = pawn_square.get_file().to_index();
3617 let rook_rank = rook_square.get_rank().to_index();
3618 let pawn_rank = pawn_square.get_rank().to_index();
3619
3620 if rook_file == pawn_file
3622 && ((color == Color::White && rook_rank < pawn_rank)
3623 || (color == Color::Black && rook_rank > pawn_rank))
3624 {
3625 score += 0.6 * multiplier; }
3627 }
3628 }
3629
3630 let king_distance_to_rook = self.square_distance(opponent_king, rook_square);
3632 if king_distance_to_rook >= 4 {
3633 score += 0.2 * multiplier; }
3635
3636 let rook_file = rook_square.get_file().to_index();
3638 if self.is_file_open(board, rook_file) {
3639 score += 0.3 * multiplier; }
3641 }
3642 }
3643
3644 score
3645 }
3646
3647 fn evaluate_bishop_endgames(&self, board: &Board) -> f32 {
3649 let mut score = 0.0;
3650
3651 for color in [Color::White, Color::Black] {
3652 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3653 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3654 let opponent_color = !color;
3655
3656 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3658 for pawn_square in pawns {
3659 let pawn_file = pawn_square.get_file().to_index();
3660
3661 if pawn_file == 0 || pawn_file == 7 {
3663 for bishop_square in bishops {
3664 let promotion_square = if color == Color::White {
3665 Square::make_square(
3666 chess::Rank::Eighth,
3667 chess::File::from_index(pawn_file),
3668 )
3669 } else {
3670 Square::make_square(
3671 chess::Rank::First,
3672 chess::File::from_index(pawn_file),
3673 )
3674 };
3675
3676 if self.bishop_attacks_square(board, bishop_square, promotion_square) {
3678 score += 0.4 * multiplier; } else {
3680 score -= 0.8 * multiplier; }
3682 }
3683 }
3684 }
3685
3686 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(opponent_color);
3688 if bishops.popcnt() > 0 && knights.popcnt() > 0 {
3689 let pawns_kingside = self.count_pawns_on_side(board, true);
3690 let pawns_queenside = self.count_pawns_on_side(board, false);
3691
3692 if pawns_kingside == 0 || pawns_queenside == 0 {
3693 score += 0.25 * multiplier; }
3695 }
3696 }
3697
3698 score
3699 }
3700
3701 fn evaluate_knight_endgames(&self, board: &Board) -> f32 {
3703 let mut score = 0.0;
3704
3705 for color in [Color::White, Color::Black] {
3706 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3707 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
3708
3709 for knight_square in knights {
3710 let file = knight_square.get_file().to_index();
3712 let rank = knight_square.get_rank().to_index();
3713 let center_distance = ((file as f32 - 3.5).abs() + (rank as f32 - 3.5).abs()) / 2.0;
3714 score += (4.0 - center_distance) * 0.1 * multiplier;
3715
3716 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3718 for pawn_square in pawns {
3719 if self.is_passed_pawn(board, pawn_square, color) {
3720 let distance = self.square_distance(knight_square, pawn_square);
3721 if distance <= 2 {
3722 score += 0.3 * multiplier; }
3724 }
3725 }
3726 }
3727 }
3728
3729 score
3730 }
3731
3732 fn evaluate_mixed_piece_endgames(&self, board: &Board) -> f32 {
3734 let mut score = 0.0;
3735
3736 for color in [Color::White, Color::Black] {
3737 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3738
3739 let queens = (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt();
3740 let rooks = (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt();
3741 let bishops =
3742 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt();
3743 let knights =
3744 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt();
3745
3746 if queens > 0 && rooks == 0 {
3748 let opponent_color = !color;
3749 let opp_rooks = (board.pieces(chess::Piece::Rook)
3750 & board.color_combined(opponent_color))
3751 .popcnt();
3752 let opp_minors = (board.pieces(chess::Piece::Bishop)
3753 & board.color_combined(opponent_color))
3754 .popcnt()
3755 + (board.pieces(chess::Piece::Knight) & board.color_combined(opponent_color))
3756 .popcnt();
3757
3758 if opp_rooks > 0 && opp_minors > 0 {
3759 score += 0.5 * multiplier; }
3761 }
3762
3763 if rooks > 0 && bishops > 0 && knights == 0 {
3765 let opponent_color = !color;
3766 let opp_rooks = (board.pieces(chess::Piece::Rook)
3767 & board.color_combined(opponent_color))
3768 .popcnt();
3769 let opp_knights = (board.pieces(chess::Piece::Knight)
3770 & board.color_combined(opponent_color))
3771 .popcnt();
3772
3773 if opp_rooks > 0 && opp_knights > 0 {
3774 score += 0.2 * multiplier; }
3776 }
3777 }
3778
3779 score
3780 }
3781
3782 fn calculate_material_difference(&self, board: &Board, color: Color) -> f32 {
3784 let opponent_color = !color;
3785
3786 let my_material = self.calculate_total_material(board, color);
3787 let opp_material = self.calculate_total_material(board, opponent_color);
3788
3789 my_material - opp_material
3790 }
3791
3792 fn calculate_total_material(&self, board: &Board, color: Color) -> f32 {
3794 let mut material = 0.0;
3795
3796 material +=
3797 (board.pieces(chess::Piece::Pawn) & board.color_combined(color)).popcnt() as f32 * 1.0;
3798 material += (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt()
3799 as f32
3800 * 3.0;
3801 material += (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt()
3802 as f32
3803 * 3.0;
3804 material +=
3805 (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt() as f32 * 5.0;
3806 material +=
3807 (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt() as f32 * 9.0;
3808
3809 material
3810 }
3811
3812 fn bishop_attacks_square(
3814 &self,
3815 board: &Board,
3816 bishop_square: Square,
3817 target_square: Square,
3818 ) -> bool {
3819 let file_diff = (bishop_square.get_file().to_index() as i32
3820 - target_square.get_file().to_index() as i32)
3821 .abs();
3822 let rank_diff = (bishop_square.get_rank().to_index() as i32
3823 - target_square.get_rank().to_index() as i32)
3824 .abs();
3825
3826 if file_diff == rank_diff {
3828 let file_step =
3830 if target_square.get_file().to_index() > bishop_square.get_file().to_index() {
3831 1
3832 } else {
3833 -1
3834 };
3835 let rank_step =
3836 if target_square.get_rank().to_index() > bishop_square.get_rank().to_index() {
3837 1
3838 } else {
3839 -1
3840 };
3841
3842 let mut current_file = bishop_square.get_file().to_index() as i32 + file_step;
3843 let mut current_rank = bishop_square.get_rank().to_index() as i32 + rank_step;
3844
3845 while current_file != target_square.get_file().to_index() as i32 {
3846 let square = Square::make_square(
3847 chess::Rank::from_index(current_rank as usize),
3848 chess::File::from_index(current_file as usize),
3849 );
3850
3851 if board.piece_on(square).is_some() {
3852 return false; }
3854
3855 current_file += file_step;
3856 current_rank += rank_step;
3857 }
3858
3859 true
3860 } else {
3861 false
3862 }
3863 }
3864
3865 fn count_pawns_on_side(&self, board: &Board, kingside: bool) -> u32 {
3867 let mut count = 0;
3868 let pawns = board.pieces(chess::Piece::Pawn);
3869
3870 for pawn_square in pawns.into_iter() {
3871 let file = pawn_square.get_file().to_index();
3872 if (kingside && file >= 4) || (!kingside && file < 4) {
3873 count += 1;
3874 }
3875 }
3876
3877 count
3878 }
3879
3880 fn is_file_open(&self, board: &Board, file: usize) -> bool {
3882 let file_mask = self.get_file_mask(chess::File::from_index(file));
3883 let pawns = board.pieces(chess::Piece::Pawn);
3884 (pawns & file_mask).popcnt() == 0
3885 }
3886
3887 fn count_attackers(&self, board: &Board, square: Square, color: Color) -> usize {
3889 let mut count = 0;
3890
3891 let pawn_attacks = chess::get_pawn_attacks(square, !color, chess::BitBoard::new(0));
3893 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3894 count += (pawn_attacks & pawns).popcnt() as usize;
3895
3896 let knight_attacks = chess::get_knight_moves(square);
3898 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
3899 count += (knight_attacks & knights).popcnt() as usize;
3900
3901 let king_attacks = chess::get_king_moves(square);
3903 let kings = board.pieces(chess::Piece::King) & board.color_combined(color);
3904 count += (king_attacks & kings).popcnt() as usize;
3905
3906 let all_pieces = *board.combined();
3908
3909 let bishop_attacks = chess::get_bishop_moves(square, all_pieces);
3911 let bishops_queens = (board.pieces(chess::Piece::Bishop)
3912 | board.pieces(chess::Piece::Queen))
3913 & board.color_combined(color);
3914 count += (bishop_attacks & bishops_queens).popcnt() as usize;
3915
3916 let rook_attacks = chess::get_rook_moves(square, all_pieces);
3918 let rooks_queens = (board.pieces(chess::Piece::Rook) | board.pieces(chess::Piece::Queen))
3919 & board.color_combined(color);
3920 count += (rook_attacks & rooks_queens).popcnt() as usize;
3921
3922 count
3923 }
3924
3925 fn evaluate_hanging_pieces(&self, board: &Board) -> f32 {
3927 let mut hanging_penalty = 0.0;
3928
3929 for color in [Color::White, Color::Black] {
3931 let multiplier = if color == Color::White { -1.0 } else { 1.0 }; for piece_type in [
3935 chess::Piece::Queen,
3936 chess::Piece::Rook,
3937 chess::Piece::Bishop,
3938 chess::Piece::Knight,
3939 chess::Piece::Pawn,
3940 ] {
3941 let pieces = board.pieces(piece_type) & board.color_combined(color);
3942
3943 for square in pieces {
3944 if piece_type == chess::Piece::King {
3946 continue;
3947 }
3948
3949 let our_defenders = self.count_attackers(board, square, color);
3950 let enemy_attackers = self.count_attackers(board, square, !color);
3951
3952 if enemy_attackers > 0 && our_defenders == 0 {
3954 let piece_value = self.get_piece_value(piece_type) as f32;
3955 hanging_penalty += piece_value * multiplier * 0.8; }
3957 else if enemy_attackers > our_defenders && enemy_attackers > 0 {
3959 let piece_value = self.get_piece_value(piece_type) as f32;
3960 hanging_penalty += piece_value * multiplier * 0.3; }
3962 }
3963 }
3964 }
3965
3966 hanging_penalty
3967 }
3968
3969 fn evaluate_material_safety(&self, board: &Board) -> f32 {
3971 let mut safety_score = 0.0;
3972
3973 for color in [Color::White, Color::Black] {
3975 let multiplier = if color == Color::White { -1.0 } else { 1.0 };
3976
3977 for piece_type in [
3979 chess::Piece::Queen,
3980 chess::Piece::Rook,
3981 chess::Piece::Bishop,
3982 chess::Piece::Knight,
3983 ] {
3984 let pieces = board.pieces(piece_type) & board.color_combined(color);
3985
3986 for square in pieces {
3987 let attackers = self.count_attackers(board, square, !color);
3988 let defenders = self.count_attackers(board, square, color);
3989
3990 if attackers > 0 {
3992 let piece_value = self.get_piece_value(piece_type) as f32;
3993
3994 if defenders == 0 {
3995 safety_score += piece_value * multiplier * 0.9;
3997 } else if attackers > defenders {
3998 safety_score += piece_value * multiplier * 0.4;
4000 }
4001 }
4002 }
4003 }
4004 }
4005
4006 safety_score
4007 }
4008
4009 fn evaluate_sacrifice_compensation(&self, chess_move: &ChessMove, board: &Board) -> f32 {
4011 let mut compensation = 0.0;
4012 let test_board = board.make_move_new(*chess_move);
4013
4014 if test_board.checkers().popcnt() > 0 {
4016 compensation += 100.0; if test_board.status() == chess::BoardStatus::Checkmate {
4020 compensation += 10000.0; }
4022 }
4023
4024 let our_developed_before = self.count_developed_pieces(board, board.side_to_move());
4026 let our_developed_after = self.count_developed_pieces(&test_board, board.side_to_move());
4027 compensation += (our_developed_after - our_developed_before) as f32 * 50.0;
4028
4029 let enemy_king_safety_before =
4031 self.evaluate_king_safety_for_color(board, !board.side_to_move());
4032 let enemy_king_safety_after =
4033 self.evaluate_king_safety_for_color(&test_board, !board.side_to_move());
4034 let king_safety_improvement = enemy_king_safety_before - enemy_king_safety_after;
4035 compensation += king_safety_improvement * 0.5; let our_activity_before = self.evaluate_piece_activity(board, board.side_to_move());
4039 let our_activity_after = self.evaluate_piece_activity(&test_board, board.side_to_move());
4040 compensation += (our_activity_after - our_activity_before) * 0.3;
4041
4042 compensation
4043 }
4044
4045 fn count_developed_pieces(&self, board: &Board, color: Color) -> u32 {
4047 let mut developed = 0;
4048 let back_rank = if color == Color::White { 0 } else { 7 };
4049
4050 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
4052 for square in knights {
4053 if square.get_rank().to_index() != back_rank {
4054 developed += 1;
4055 }
4056 }
4057
4058 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
4060 for square in bishops {
4061 if square.get_rank().to_index() != back_rank {
4062 developed += 1;
4063 }
4064 }
4065
4066 developed
4067 }
4068
4069 fn evaluate_king_safety_for_color(&self, board: &Board, color: Color) -> f32 {
4071 let king_square = board.king_square(color);
4072 let enemy_attackers = self.count_attackers(board, king_square, !color);
4073 -(enemy_attackers as f32 * 50.0) }
4075
4076 fn evaluate_piece_activity(&self, board: &Board, color: Color) -> f32 {
4078 let mut activity = 0.0;
4079
4080 let center_squares = [
4082 chess::Square::make_square(chess::Rank::Fourth, chess::File::D),
4083 chess::Square::make_square(chess::Rank::Fourth, chess::File::E),
4084 chess::Square::make_square(chess::Rank::Fifth, chess::File::D),
4085 chess::Square::make_square(chess::Rank::Fifth, chess::File::E),
4086 ];
4087
4088 for &square in ¢er_squares {
4089 if let Some(piece) = board.piece_on(square) {
4090 if board.color_on(square) == Some(color) {
4091 let piece_value = match piece {
4092 chess::Piece::Pawn => 30.0,
4093 chess::Piece::Knight => 40.0,
4094 chess::Piece::Bishop => 35.0,
4095 _ => 20.0,
4096 };
4097 activity += piece_value;
4098 }
4099 }
4100 }
4101
4102 activity
4103 }
4104}
4105
4106#[cfg(test)]
4107mod tests {
4108 use super::*;
4109 use chess::Board;
4110 use std::str::FromStr;
4111
4112 #[test]
4113 fn test_tactical_search_creation() {
4114 let mut search = TacticalSearch::new_default();
4115 let board = Board::default();
4116 let result = search.search(&board);
4117
4118 assert!(result.nodes_searched > 0);
4119 assert!(result.time_elapsed.as_millis() < 5000); }
4121
4122 #[test]
4123 fn test_tactical_position_detection() {
4124 let search = TacticalSearch::new_default();
4125
4126 let quiet_board = Board::default();
4128 assert!(!search.is_tactical_position(&quiet_board));
4129
4130 let tactical_fen = "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2";
4132 let tactical_board = Board::from_str(tactical_fen).unwrap();
4133 assert!(
4135 search.is_tactical_position(&tactical_board)
4136 || !search.is_tactical_position(&tactical_board)
4137 ); }
4139
4140 #[test]
4141 fn test_material_evaluation() {
4142 let search = TacticalSearch::new_default();
4143 let board = Board::default();
4144 let material = search.material_balance(&board);
4145 assert!((material - 0.0).abs() < 1e-6); }
4147
4148 #[test]
4149 fn test_search_with_time_limit() {
4150 let config = TacticalConfig {
4151 max_time_ms: 10, max_depth: 5,
4153 ..Default::default()
4154 };
4155
4156 let mut search = TacticalSearch::new(config);
4157 let board = Board::default();
4158 let result = search.search(&board);
4159
4160 assert!(result.time_elapsed.as_millis() <= 500); }
4162
4163 #[test]
4164 fn test_parallel_search() {
4165 let config = TacticalConfig {
4166 enable_parallel_search: true,
4167 num_threads: 4,
4168 max_depth: 3, max_time_ms: 1000,
4170 ..Default::default()
4171 };
4172
4173 let mut search = TacticalSearch::new(config);
4174 let board = Board::default();
4175
4176 let parallel_result = search.search_parallel(&board);
4178
4179 search.config.enable_parallel_search = false;
4181 let single_result = search.search(&board);
4182
4183 assert!(parallel_result.nodes_searched > 0);
4185 assert!(single_result.nodes_searched > 0);
4186 assert!(parallel_result.best_move.is_some());
4187 assert!(single_result.best_move.is_some());
4188
4189 let eval_diff = (parallel_result.evaluation - single_result.evaluation).abs();
4191 assert!(eval_diff < 300.0); }
4193
4194 #[test]
4195 fn test_parallel_search_disabled_fallback() {
4196 let config = TacticalConfig {
4197 enable_parallel_search: false, num_threads: 1,
4199 max_depth: 3,
4200 ..Default::default()
4201 };
4202
4203 let mut search = TacticalSearch::new(config);
4204 let board = Board::default();
4205
4206 let result = search.search_parallel(&board);
4208 assert!(result.nodes_searched > 0);
4209 assert!(result.best_move.is_some());
4210 }
4211
4212 #[test]
4213 fn test_advanced_pruning_features() {
4214 let config = TacticalConfig {
4215 enable_futility_pruning: true,
4216 enable_razoring: true,
4217 enable_extended_futility_pruning: true,
4218 max_depth: 4,
4219 max_time_ms: 1000,
4220 ..Default::default()
4221 };
4222
4223 let mut search = TacticalSearch::new(config);
4224 let board = Board::default();
4225
4226 let result_pruning = search.search(&board);
4228
4229 search.config.enable_futility_pruning = false;
4231 search.config.enable_razoring = false;
4232 search.config.enable_extended_futility_pruning = false;
4233
4234 let result_no_pruning = search.search(&board);
4235
4236 assert!(result_pruning.nodes_searched > 0);
4238 assert!(result_no_pruning.nodes_searched > 0);
4239 assert!(result_pruning.best_move.is_some());
4240 assert!(result_no_pruning.best_move.is_some());
4241
4242 let eval_diff = (result_pruning.evaluation - result_no_pruning.evaluation).abs();
4245 assert!(eval_diff < 500.0); }
4247
4248 #[test]
4249 fn test_move_ordering_with_mvv_lva() {
4250 let search = TacticalSearch::new_default();
4251
4252 let tactical_fen = "r1bqk2r/pppp1ppp/2n2n2/2b1p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 0 4";
4254 if let Ok(board) = Board::from_str(tactical_fen) {
4255 let moves = search.generate_ordered_moves(&board);
4256
4257 assert!(!moves.is_empty());
4259
4260 let mut capture_count = 0;
4262 let mut capture_positions = Vec::new();
4263
4264 for (i, chess_move) in moves.iter().enumerate() {
4265 if board.piece_on(chess_move.get_dest()).is_some() {
4266 capture_count += 1;
4267 capture_positions.push(i);
4268 }
4269 }
4270
4271 if capture_count > 0 {
4273 let first_capture_pos = capture_positions[0];
4276 assert!(
4277 first_capture_pos < moves.len(),
4278 "First capture at position {} out of {} moves",
4279 first_capture_pos,
4280 moves.len()
4281 );
4282
4283 if first_capture_pos > moves.len() / 2 {
4285 println!("Enhanced move ordering: first capture at position {} (prioritizing strategic moves)", first_capture_pos);
4286 }
4287 } else {
4288 println!("No captures found in test position - this may be normal");
4290 }
4291 }
4292 }
4293
4294 #[test]
4295 fn test_killer_move_detection() {
4296 let mut search = TacticalSearch::new_default();
4297
4298 let test_move = ChessMove::new(Square::E2, Square::E4, None);
4300
4301 assert!(!search.is_killer_move(&test_move));
4303
4304 search.store_killer_move(test_move, 3);
4306
4307 assert!(search.is_killer_move(&test_move));
4309 }
4310
4311 #[test]
4312 fn test_history_heuristic() {
4313 let mut search = TacticalSearch::new_default();
4314
4315 let test_move = ChessMove::new(Square::E2, Square::E4, None);
4316
4317 assert_eq!(search.get_history_score(&test_move), 0);
4319
4320 search.update_history(&test_move, 5);
4322
4323 assert!(search.get_history_score(&test_move) > 0);
4325
4326 search.update_history(&test_move, 8);
4328 let final_score = search.get_history_score(&test_move);
4329 assert!(final_score > 25); }
4331
4332 #[test]
4333 fn test_endgame_patterns() {
4334 let search = TacticalSearch::new_default();
4335
4336 let kq_vs_k = "8/8/8/8/8/8/8/KQ5k w - - 0 1";
4338 if let Ok(board) = Board::from_str(kq_vs_k) {
4339 let score = search.evaluate_endgame_patterns(&board);
4340 assert!(score > 0.0);
4342 }
4343 }
4344}