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
135impl Default for TacticalConfig {
136 fn default() -> Self {
137 Self {
138 max_depth: 14, max_time_ms: 8000, max_nodes: 2_000_000, quiescence_depth: 12, enable_transposition_table: true,
146 enable_iterative_deepening: true,
147 enable_aspiration_windows: true, enable_null_move_pruning: true,
149 enable_late_move_reductions: true,
150 enable_principal_variation_search: true,
151 enable_parallel_search: true,
152 num_threads: 4,
153
154 enable_futility_pruning: true,
156 enable_razoring: true,
157 enable_extended_futility_pruning: true,
158 futility_margin_base: 150.0, razor_margin: 300.0, extended_futility_margin: 60.0, null_move_reduction_depth: 3, lmr_min_depth: 3, lmr_min_moves: 4, 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, }
188 }
189}
190
191impl TacticalConfig {
192 pub fn fast() -> Self {
194 Self {
195 max_depth: 8,
196 max_time_ms: 1000,
197 max_nodes: 200_000,
198 quiescence_depth: 4,
199 aspiration_window_size: 75.0,
200 transposition_table_size_mb: 32,
201 ..Default::default()
202 }
203 }
204
205 pub fn strong() -> Self {
207 Self {
208 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()
216 }
217 }
218
219 pub fn analysis() -> Self {
221 Self {
222 max_depth: 20,
223 max_time_ms: 60_000, max_nodes: 10_000_000, quiescence_depth: 10,
226 enable_aspiration_windows: false, transposition_table_size_mb: 512,
228 num_threads: std::thread::available_parallelism()
229 .map(|n| n.get())
230 .unwrap_or(4),
231 ..Default::default()
232 }
233 }
234}
235
236#[derive(Debug, Clone)]
238struct TranspositionEntry {
239 depth: u32,
240 evaluation: f32,
241 best_move: Option<ChessMove>,
242 node_type: NodeType,
243 age: u8, }
245
246#[derive(Debug, Clone, Copy)]
247enum NodeType {
248 Exact,
249 LowerBound,
250 UpperBound,
251}
252
253#[derive(Clone)]
255pub struct TacticalSearch {
256 pub config: TacticalConfig,
257 transposition_table: FixedTranspositionTable,
258 nodes_searched: u64,
259 start_time: Instant,
260 killer_moves: Vec<Vec<Option<ChessMove>>>, history_heuristic: HashMap<(Square, Square), u32>,
264}
265
266impl TacticalSearch {
267 pub fn new(config: TacticalConfig) -> Self {
269 let max_depth = config.max_depth as usize + 1;
270 Self {
271 config,
272 transposition_table: FixedTranspositionTable::new(64), nodes_searched: 0,
274 start_time: Instant::now(),
275 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
277 }
278 }
279
280 pub fn with_table_size(config: TacticalConfig, table_size_mb: usize) -> Self {
282 let max_depth = config.max_depth as usize + 1;
283 Self {
284 config,
285 transposition_table: FixedTranspositionTable::new(table_size_mb),
286 nodes_searched: 0,
287 start_time: Instant::now(),
288 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
290 }
291 }
292
293 pub fn new_default() -> Self {
295 Self::new(TacticalConfig::default())
296 }
297
298 pub fn search(&mut self, board: &Board) -> TacticalResult {
300 self.nodes_searched = 0;
301 self.start_time = Instant::now();
302 self.transposition_table.clear();
303
304 let is_tactical = self.is_tactical_position(board);
306
307 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
308 self.iterative_deepening_search(board)
309 } else {
310 let (eval, mv) = self.minimax(
311 board,
312 self.config.max_depth,
313 f32::NEG_INFINITY,
314 f32::INFINITY,
315 board.side_to_move() == Color::White,
316 );
317 (eval, mv, self.config.max_depth)
318 };
319
320 TacticalResult {
321 evaluation,
322 best_move,
323 depth_reached,
324 nodes_searched: self.nodes_searched,
325 time_elapsed: self.start_time.elapsed(),
326 is_tactical,
327 }
328 }
329
330 pub fn search_parallel(&mut self, board: &Board) -> TacticalResult {
332 if !self.config.enable_parallel_search || self.config.num_threads <= 1 {
333 return self.search(board); }
335
336 self.nodes_searched = 0;
337 self.start_time = Instant::now();
338 self.transposition_table.clear();
339
340 let is_tactical = self.is_tactical_position(board);
341 let moves = self.generate_ordered_moves(board);
342
343 if moves.is_empty() {
344 return TacticalResult {
345 evaluation: self.evaluate_terminal_position(board),
346 best_move: None,
347 depth_reached: 1,
348 nodes_searched: 1,
349 time_elapsed: self.start_time.elapsed(),
350 is_tactical,
351 };
352 }
353
354 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
356 self.parallel_iterative_deepening(board, moves)
357 } else {
358 self.parallel_root_search(board, moves, self.config.max_depth)
359 };
360
361 TacticalResult {
362 evaluation,
363 best_move,
364 depth_reached,
365 nodes_searched: self.nodes_searched,
366 time_elapsed: self.start_time.elapsed(),
367 is_tactical,
368 }
369 }
370
371 fn parallel_root_search(
373 &mut self,
374 board: &Board,
375 moves: Vec<ChessMove>,
376 depth: u32,
377 ) -> (f32, Option<ChessMove>, u32) {
378 let maximizing = board.side_to_move() == Color::White;
379 let nodes_counter = Arc::new(Mutex::new(0u64));
380
381 let move_scores: Vec<(ChessMove, f32)> = moves
383 .into_par_iter()
384 .map(|mv| {
385 let new_board = board.make_move_new(mv);
386 let mut search_clone = self.clone();
387 search_clone.nodes_searched = 0;
388
389 let (eval, _) = search_clone.minimax(
390 &new_board,
391 depth - 1,
392 f32::NEG_INFINITY,
393 f32::INFINITY,
394 !maximizing,
395 );
396
397 if let Ok(mut counter) = nodes_counter.lock() {
399 *counter += search_clone.nodes_searched;
400 }
401
402 (mv, -eval)
404 })
405 .collect();
406
407 if let Ok(counter) = nodes_counter.lock() {
409 self.nodes_searched = *counter;
410 }
411
412 let best = move_scores
414 .into_iter()
415 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
416
417 match best {
418 Some((best_move, best_eval)) => (best_eval, Some(best_move), depth),
419 None => (0.0, None, depth),
420 }
421 }
422
423 fn parallel_iterative_deepening(
425 &mut self,
426 board: &Board,
427 mut moves: Vec<ChessMove>,
428 ) -> (f32, Option<ChessMove>, u32) {
429 let mut best_move: Option<ChessMove> = None;
430 let mut best_evaluation = 0.0;
431 let mut completed_depth = 0;
432
433 for depth in 1..=self.config.max_depth {
434 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128 {
436 break;
437 }
438
439 let (eval, mv, _) = self.parallel_root_search(board, moves.clone(), depth);
440
441 best_evaluation = eval;
442 best_move = mv;
443 completed_depth = depth;
444
445 if let Some(best) = best_move {
447 if let Some(pos) = moves.iter().position(|&m| m == best) {
448 moves.swap(0, pos);
449 }
450 }
451 }
452
453 (best_evaluation, best_move, completed_depth)
454 }
455
456 fn iterative_deepening_search(&mut self, board: &Board) -> (f32, Option<ChessMove>, u32) {
458 let mut best_move: Option<ChessMove> = None;
459 let mut best_evaluation = 0.0;
460 let mut completed_depth = 0;
461
462 let position_complexity = self.calculate_position_complexity(board);
464 let base_time_per_depth = self.config.max_time_ms as f32 / self.config.max_depth as f32;
465 let adaptive_time_factor = 0.5 + (position_complexity * 1.5); for depth in 1..=self.config.max_depth {
468 let depth_start_time = std::time::Instant::now();
469
470 let depth_time_budget = (base_time_per_depth
472 * adaptive_time_factor
473 * (1.0 + (depth as f32 - 1.0) * 0.3)) as u64;
474
475 let elapsed = self.start_time.elapsed().as_millis() as u64;
477 if elapsed + depth_time_budget > self.config.max_time_ms {
478 break;
480 }
481
482 let window_size = if self.config.enable_aspiration_windows && depth > 2 {
483 50.0 } else {
485 f32::INFINITY
486 };
487
488 let (evaluation, mv) = if self.config.enable_aspiration_windows && depth > 2 {
489 self.aspiration_window_search(board, depth, best_evaluation, window_size)
490 } else {
491 self.minimax(
492 board,
493 depth,
494 f32::NEG_INFINITY,
495 f32::INFINITY,
496 board.side_to_move() == Color::White,
497 )
498 };
499
500 best_evaluation = evaluation;
502 if mv.is_some() {
503 best_move = mv;
504 }
505 completed_depth = depth;
506
507 if evaluation.abs() > 9000.0 {
509 break;
510 }
511
512 let depth_time_taken = depth_start_time.elapsed().as_millis() as u64;
515 let remaining_time = self
516 .config
517 .max_time_ms
518 .saturating_sub(elapsed + depth_time_taken);
519
520 if depth < self.config.max_depth {
522 let estimated_next_depth_time = depth_time_taken * 3; if estimated_next_depth_time > remaining_time {
524 break;
525 }
526 }
527 }
528
529 (best_evaluation, best_move, completed_depth)
530 }
531
532 fn calculate_position_complexity(&self, board: &Board) -> f32 {
534 let mut complexity = 0.0;
535
536 let total_pieces = board.combined().popcnt() as f32;
538 complexity += (total_pieces - 20.0) / 12.0; let legal_moves = MoveGen::new_legal(board).count() as f32;
542 complexity += (legal_moves - 20.0) / 20.0; if board.checkers().popcnt() > 0 {
546 complexity += 0.5;
547 }
548
549 if self.is_tactical_position(board) {
551 complexity += 0.3;
552 }
553
554 let game_phase = self.determine_game_phase(board);
556 if game_phase == GamePhase::Endgame {
557 complexity -= 0.3;
558 }
559
560 complexity.clamp(0.2, 1.5)
562 }
563
564 fn aspiration_window_search(
566 &mut self,
567 board: &Board,
568 depth: u32,
569 prev_score: f32,
570 window: f32,
571 ) -> (f32, Option<ChessMove>) {
572 let mut alpha = prev_score - window;
573 let mut beta = prev_score + window;
574
575 loop {
576 let (score, mv) = self.minimax(
577 board,
578 depth,
579 alpha,
580 beta,
581 board.side_to_move() == Color::White,
582 );
583
584 if score <= alpha {
585 alpha = f32::NEG_INFINITY;
587 } else if score >= beta {
588 beta = f32::INFINITY;
590 } else {
591 return (score, mv);
593 }
594 }
595 }
596
597 fn minimax(
599 &mut self,
600 board: &Board,
601 depth: u32,
602 alpha: f32,
603 beta: f32,
604 maximizing: bool,
605 ) -> (f32, Option<ChessMove>) {
606 self.minimax_with_extensions(board, depth, alpha, beta, maximizing, 0)
607 }
608
609 fn minimax_with_extensions(
610 &mut self,
611 board: &Board,
612 depth: u32,
613 alpha: f32,
614 beta: f32,
615 maximizing: bool,
616 extensions_used: u32,
617 ) -> (f32, Option<ChessMove>) {
618 self.nodes_searched += 1;
619
620 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128
622 || self.nodes_searched > self.config.max_nodes
623 {
624 return (self.evaluate_position(board), None);
625 }
626
627 let mut actual_depth = depth;
629 if self.config.enable_check_extensions
630 && board.checkers().popcnt() > 0
631 && extensions_used < self.config.max_extensions_per_line
632 {
633 actual_depth += self.config.check_extension_depth;
634 }
635
636 if actual_depth == 0 {
638 return (
639 self.quiescence_search(
640 board,
641 self.config.quiescence_depth,
642 alpha,
643 beta,
644 maximizing,
645 ),
646 None,
647 );
648 }
649
650 if board.status() != chess::BoardStatus::Ongoing {
651 return (self.evaluate_terminal_position(board), None);
652 }
653
654 if self.config.enable_transposition_table {
656 if let Some(entry) = self.transposition_table.get(board.get_hash()) {
657 if entry.depth >= depth {
658 match entry.node_type {
659 NodeType::Exact => return (entry.evaluation, entry.best_move),
660 NodeType::LowerBound if entry.evaluation >= beta => {
661 return (entry.evaluation, entry.best_move)
662 }
663 NodeType::UpperBound if entry.evaluation <= alpha => {
664 return (entry.evaluation, entry.best_move)
665 }
666 _ => {}
667 }
668 }
669 }
670 }
671
672 let static_eval = self.evaluate_position(board);
674
675 if self.config.enable_razoring
677 && (1..=3).contains(&depth)
678 && !maximizing && static_eval + self.config.razor_margin < alpha
680 {
681 let razor_eval = self.quiescence_search(board, 1, alpha, beta, maximizing);
683 if razor_eval < alpha {
684 return (razor_eval, None);
685 }
686 }
687
688 if self.config.enable_futility_pruning
690 && depth == 1
691 && !maximizing
692 && board.checkers().popcnt() == 0 && static_eval + self.config.futility_margin_base < alpha
694 {
695 return (static_eval, None);
697 }
698
699 if self.config.enable_extended_futility_pruning
701 && (2..=4).contains(&depth)
702 && !maximizing
703 && board.checkers().popcnt() == 0 && static_eval + self.config.extended_futility_margin * (depth as f32) < alpha
705 {
706 return (static_eval, None);
708 }
709
710 if self.config.enable_null_move_pruning
712 && depth >= 3
713 && !maximizing && board.checkers().popcnt() == 0 && self.has_non_pawn_material(board, board.side_to_move())
716 {
717 let null_move_reduction = (depth / 4).clamp(2, 4);
718 let new_depth = depth.saturating_sub(null_move_reduction);
719
720 let null_board = board.null_move().unwrap_or(*board);
722 let (null_score, _) = self.minimax(&null_board, new_depth, alpha, beta, !maximizing);
723
724 if null_score >= beta {
726 return (beta, None);
727 }
728 }
729
730 let hash_move = if self.config.enable_transposition_table {
732 self.transposition_table
733 .get(board.get_hash())
734 .and_then(|entry| entry.best_move)
735 } else {
736 None
737 };
738
739 let moves = self.generate_ordered_moves_with_hash(board, hash_move, depth);
741
742 let (best_value, best_move) =
743 if self.config.enable_principal_variation_search && moves.len() > 1 {
744 self.principal_variation_search(board, depth, alpha, beta, maximizing, moves)
746 } else {
747 self.alpha_beta_search(board, depth, alpha, beta, maximizing, moves)
749 };
750
751 if self.config.enable_transposition_table {
753 let node_type = if best_value <= alpha {
754 NodeType::UpperBound
755 } else if best_value >= beta {
756 NodeType::LowerBound
757 } else {
758 NodeType::Exact
759 };
760
761 self.transposition_table.insert(
762 board.get_hash(),
763 TranspositionEntry {
764 depth,
765 evaluation: best_value,
766 best_move,
767 node_type,
768 age: 0, },
770 );
771 }
772
773 (best_value, best_move)
774 }
775
776 fn principal_variation_search(
778 &mut self,
779 board: &Board,
780 depth: u32,
781 mut alpha: f32,
782 mut beta: f32,
783 maximizing: bool,
784 moves: Vec<ChessMove>,
785 ) -> (f32, Option<ChessMove>) {
786 let mut best_move: Option<ChessMove> = None;
787 let mut best_value = if maximizing {
788 f32::NEG_INFINITY
789 } else {
790 f32::INFINITY
791 };
792 let mut _pv_found = false;
793 let mut first_move = true;
794
795 if moves.is_empty() {
797 return (self.evaluate_position(board), None);
798 }
799
800 for (move_index, chess_move) in moves.into_iter().enumerate() {
801 let new_board = board.make_move_new(chess_move);
802 let mut evaluation;
803
804 let reduction = if self.config.enable_late_move_reductions
806 && depth >= 3
807 && move_index >= 2 && !self.is_capture_or_promotion(&chess_move, board)
809 && new_board.checkers().popcnt() == 0 && !self.is_killer_move(&chess_move)
811 {
812 let base_reduction = if move_index >= 6 { 2 } else { 1 };
816 let depth_factor = (depth as f32 / 3.0) as u32;
817 let move_factor = ((move_index as f32).ln() / 2.0) as u32;
818
819 base_reduction + depth_factor + move_factor
820 } else {
821 0
822 };
823
824 let search_depth = if depth > reduction {
825 depth - 1 - reduction
826 } else {
827 0
828 };
829
830 if move_index == 0 {
831 let search_depth = if depth > 0 { depth - 1 } else { 0 };
833 let (eval, _) = self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
834 evaluation = eval;
835 _pv_found = true;
836 } else {
837 let null_window_alpha = if maximizing { alpha } else { beta - 1.0 };
839 let null_window_beta = if maximizing { alpha + 1.0 } else { beta };
840
841 let (null_eval, _) = self.minimax(
842 &new_board,
843 search_depth,
844 null_window_alpha,
845 null_window_beta,
846 !maximizing,
847 );
848
849 if null_eval > alpha && null_eval < beta {
851 let full_depth = if reduction > 0 {
853 if depth > 0 {
854 depth - 1
855 } else {
856 0
857 }
858 } else {
859 search_depth
860 };
861 let (full_eval, _) =
862 self.minimax(&new_board, full_depth, alpha, beta, !maximizing);
863 evaluation = full_eval;
864 } else {
865 evaluation = null_eval;
866
867 if reduction > 0
869 && ((maximizing && evaluation > alpha)
870 || (!maximizing && evaluation < beta))
871 {
872 let search_depth = if depth > 0 { depth - 1 } else { 0 };
873 let (re_eval, _) =
874 self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
875 evaluation = re_eval;
876 }
877 }
878 }
879
880 if maximizing {
882 if first_move || evaluation > best_value {
883 best_value = evaluation;
884 best_move = Some(chess_move);
885 }
886 alpha = alpha.max(evaluation);
887 } else {
888 if first_move || evaluation < best_value {
889 best_value = evaluation;
890 best_move = Some(chess_move);
891 }
892 beta = beta.min(evaluation);
893 }
894
895 first_move = false;
896
897 if beta <= alpha {
899 if !self.is_capture_or_promotion(&chess_move, board) {
901 self.store_killer_move(chess_move, depth);
902 self.update_history(&chess_move, depth);
903 }
904 break;
905 }
906 }
907
908 (best_value, best_move)
909 }
910
911 fn alpha_beta_search(
913 &mut self,
914 board: &Board,
915 depth: u32,
916 mut alpha: f32,
917 mut beta: f32,
918 maximizing: bool,
919 moves: Vec<ChessMove>,
920 ) -> (f32, Option<ChessMove>) {
921 let mut best_move: Option<ChessMove> = None;
922 let mut best_value = if maximizing {
923 f32::NEG_INFINITY
924 } else {
925 f32::INFINITY
926 };
927 let mut first_move = true;
928
929 if moves.is_empty() {
931 return (self.evaluate_position(board), None);
932 }
933
934 for (move_index, chess_move) in moves.into_iter().enumerate() {
935 let new_board = board.make_move_new(chess_move);
936
937 let reduction = if self.config.enable_late_move_reductions
939 && depth >= 3
940 && move_index >= 2 && !self.is_capture_or_promotion(&chess_move, board)
942 && new_board.checkers().popcnt() == 0 && !self.is_killer_move(&chess_move)
944 {
945 let base_reduction = if move_index >= 6 { 2 } else { 1 };
949 let depth_factor = (depth as f32 / 3.0) as u32;
950 let move_factor = ((move_index as f32).ln() / 2.0) as u32;
951
952 base_reduction + depth_factor + move_factor
953 } else {
954 0
955 };
956
957 let search_depth = if depth > reduction {
958 depth - 1 - reduction
959 } else {
960 0
961 };
962
963 let (evaluation, _) = self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
964
965 let final_evaluation = if reduction > 0
967 && ((maximizing && evaluation > alpha) || (!maximizing && evaluation < beta))
968 {
969 let search_depth = if depth > 0 { depth - 1 } else { 0 };
970 let (re_eval, _) = self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
971 re_eval
972 } else {
973 evaluation
974 };
975
976 if maximizing {
977 if first_move || final_evaluation > best_value {
978 best_value = final_evaluation;
979 best_move = Some(chess_move);
980 }
981 alpha = alpha.max(final_evaluation);
982 } else {
983 if first_move || final_evaluation < best_value {
984 best_value = final_evaluation;
985 best_move = Some(chess_move);
986 }
987 beta = beta.min(final_evaluation);
988 }
989
990 first_move = false;
991
992 if beta <= alpha {
994 if !self.is_capture_or_promotion(&chess_move, board) {
996 self.store_killer_move(chess_move, depth);
997 self.update_history(&chess_move, depth);
998 }
999 break;
1000 }
1001 }
1002
1003 (best_value, best_move)
1004 }
1005
1006 fn quiescence_search(
1008 &mut self,
1009 board: &Board,
1010 depth: u32,
1011 mut alpha: f32,
1012 beta: f32,
1013 maximizing: bool,
1014 ) -> f32 {
1015 self.nodes_searched += 1;
1016
1017 let stand_pat = self.evaluate_position(board);
1018
1019 if depth == 0 {
1020 return stand_pat;
1021 }
1022
1023 if maximizing {
1024 if stand_pat >= beta {
1025 return beta;
1026 }
1027 alpha = alpha.max(stand_pat);
1028 } else if stand_pat <= alpha {
1029 return alpha;
1030 }
1031
1032 let captures = self.generate_captures(board);
1034
1035 for chess_move in captures {
1036 let new_board = board.make_move_new(chess_move);
1037 let evaluation =
1038 self.quiescence_search(&new_board, depth - 1, alpha, beta, !maximizing);
1039
1040 if maximizing {
1041 alpha = alpha.max(evaluation);
1042 if alpha >= beta {
1043 break;
1044 }
1045 } else if evaluation <= alpha {
1046 return alpha;
1047 }
1048 }
1049
1050 stand_pat
1051 }
1052
1053 fn generate_ordered_moves(&self, board: &Board) -> Vec<ChessMove> {
1055 self.generate_ordered_moves_with_hash(board, None, 1) }
1057
1058 fn generate_ordered_moves_with_hash(
1060 &self,
1061 board: &Board,
1062 hash_move: Option<ChessMove>,
1063 depth: u32,
1064 ) -> Vec<ChessMove> {
1065 let mut moves: Vec<_> = MoveGen::new_legal(board).collect();
1066
1067 moves.sort_by(|a, b| {
1069 let a_score = self.get_move_order_score(a, board, hash_move, depth);
1070 let b_score = self.get_move_order_score(b, board, hash_move, depth);
1071 b_score.cmp(&a_score) });
1073
1074 moves
1075 }
1076
1077 fn get_move_order_score(
1079 &self,
1080 chess_move: &ChessMove,
1081 board: &Board,
1082 hash_move: Option<ChessMove>,
1083 depth: u32,
1084 ) -> i32 {
1085 if let Some(hash) = hash_move {
1087 if hash == *chess_move {
1088 return 1_000_000; }
1090 }
1091
1092 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
1094 let mvv_lva_score = self.mvv_lva_score(chess_move, board);
1095
1096 if self.is_good_capture(chess_move, board, captured_piece) {
1098 return 900_000 + mvv_lva_score; } else {
1100 return 100_000 + mvv_lva_score; }
1102 }
1103
1104 if chess_move.get_promotion().is_some() {
1106 let promotion_piece = chess_move.get_promotion().unwrap();
1107 let promotion_value = match promotion_piece {
1108 chess::Piece::Queen => 800_000,
1109 chess::Piece::Rook => 700_000,
1110 chess::Piece::Bishop => 600_000,
1111 chess::Piece::Knight => 590_000,
1112 _ => 500_000,
1113 };
1114 return promotion_value;
1115 }
1116
1117 if self.is_killer_move_at_depth(chess_move, depth) {
1119 return 500_000;
1120 }
1121
1122 if self.is_counter_move(chess_move) {
1124 return 400_000;
1125 }
1126
1127 if self.is_castling_move(chess_move, board) {
1129 return 250_000; }
1131
1132 if self.gives_check(chess_move, board) {
1134 if let Some(captured_piece) = board.piece_on(chess_move.get_dest()) {
1136 if let Some(attacker_piece) = board.piece_on(chess_move.get_source()) {
1138 let victim_value = self.get_piece_value(captured_piece);
1139 let attacker_value = self.get_piece_value(attacker_piece);
1140 if victim_value < attacker_value {
1141 return 150_000; }
1144 }
1145 return 300_000; } else {
1147 return 280_000;
1149 }
1150 }
1151
1152 let history_score = self.get_history_score(chess_move);
1154 200_000 + history_score as i32 }
1156
1157 fn is_good_capture(
1159 &self,
1160 chess_move: &ChessMove,
1161 board: &Board,
1162 captured_piece: chess::Piece,
1163 ) -> bool {
1164 let attacker_piece = board.piece_on(chess_move.get_source());
1165 if attacker_piece.is_none() {
1166 return false;
1167 }
1168
1169 let attacker_value = self.get_piece_value(attacker_piece.unwrap());
1170 let victim_value = self.get_piece_value(captured_piece);
1171
1172 victim_value >= attacker_value
1175 }
1176
1177 fn get_piece_value(&self, piece: chess::Piece) -> i32 {
1179 match piece {
1180 chess::Piece::Pawn => 100,
1181 chess::Piece::Knight => 320,
1182 chess::Piece::Bishop => 330,
1183 chess::Piece::Rook => 500,
1184 chess::Piece::Queen => 900,
1185 chess::Piece::King => 10000,
1186 }
1187 }
1188
1189 fn is_killer_move_at_depth(&self, chess_move: &ChessMove, depth: u32) -> bool {
1191 let depth_idx = (depth as usize).min(self.killer_moves.len() - 1);
1192 self.killer_moves[depth_idx].contains(&Some(*chess_move))
1193 }
1194
1195 fn is_counter_move(&self, _chess_move: &ChessMove) -> bool {
1197 false
1200 }
1201
1202 fn is_castling_move(&self, chess_move: &ChessMove, board: &Board) -> bool {
1204 if let Some(piece) = board.piece_on(chess_move.get_source()) {
1205 if piece == chess::Piece::King {
1206 let source_file = chess_move.get_source().get_file().to_index();
1207 let dest_file = chess_move.get_dest().get_file().to_index();
1208 return (source_file as i32 - dest_file as i32).abs() == 2;
1210 }
1211 }
1212 false
1213 }
1214
1215 fn gives_check(&self, chess_move: &ChessMove, board: &Board) -> bool {
1217 let new_board = board.make_move_new(*chess_move);
1218 new_board.checkers().popcnt() > 0
1219 }
1220
1221 fn mvv_lva_score(&self, chess_move: &ChessMove, board: &Board) -> i32 {
1223 let victim_value = if let Some(victim_piece) = board.piece_on(chess_move.get_dest()) {
1224 match victim_piece {
1225 chess::Piece::Pawn => 100,
1226 chess::Piece::Knight => 300,
1227 chess::Piece::Bishop => 300,
1228 chess::Piece::Rook => 500,
1229 chess::Piece::Queen => 900,
1230 chess::Piece::King => 10000, }
1232 } else {
1233 0
1234 };
1235
1236 let attacker_value = if let Some(attacker_piece) = board.piece_on(chess_move.get_source()) {
1237 match attacker_piece {
1238 chess::Piece::Pawn => 1,
1239 chess::Piece::Knight => 3,
1240 chess::Piece::Bishop => 3,
1241 chess::Piece::Rook => 5,
1242 chess::Piece::Queen => 9,
1243 chess::Piece::King => 1, }
1245 } else {
1246 1
1247 };
1248
1249 victim_value * 10 - attacker_value
1251 }
1252
1253 fn generate_captures(&self, board: &Board) -> Vec<ChessMove> {
1255 MoveGen::new_legal(board)
1256 .filter(|chess_move| {
1257 board.piece_on(chess_move.get_dest()).is_some()
1259 || chess_move.get_promotion().is_some()
1260 })
1261 .collect()
1262 }
1263
1264 fn evaluate_position(&self, board: &Board) -> f32 {
1266 if board.status() != chess::BoardStatus::Ongoing {
1267 return self.evaluate_terminal_position(board);
1268 }
1269
1270 let mut score = 0.0;
1271
1272 score += self.material_balance(board);
1274
1275 score += self.tactical_bonuses(board);
1277
1278 score += self.king_safety(board);
1280
1281 score += self.evaluate_pawn_structure(board);
1283
1284 score += self.evaluate_endgame_patterns(board);
1286
1287 score
1291 }
1292
1293 fn evaluate_terminal_position(&self, board: &Board) -> f32 {
1295 match board.status() {
1296 chess::BoardStatus::Checkmate => {
1297 if board.side_to_move() == Color::White {
1298 -10.0 } else {
1300 10.0 }
1302 }
1303 chess::BoardStatus::Stalemate => 0.0,
1304 _ => 0.0,
1305 }
1306 }
1307
1308 fn material_balance(&self, board: &Board) -> f32 {
1310 let piece_values = [
1311 (chess::Piece::Pawn, 100.0),
1312 (chess::Piece::Knight, 320.0), (chess::Piece::Bishop, 330.0), (chess::Piece::Rook, 500.0),
1315 (chess::Piece::Queen, 900.0),
1316 ];
1317
1318 let mut balance = 0.0;
1319
1320 for (piece, value) in piece_values.iter() {
1321 let white_count = board.pieces(*piece) & board.color_combined(Color::White);
1322 let black_count = board.pieces(*piece) & board.color_combined(Color::Black);
1323
1324 balance += (white_count.popcnt() as f32 - black_count.popcnt() as f32) * value;
1325 }
1326
1327 balance += self.piece_square_evaluation(board);
1329
1330 balance / 100.0 }
1332
1333 fn piece_square_evaluation(&self, board: &Board) -> f32 {
1335 let mut score = 0.0;
1336 let game_phase = self.detect_game_phase(board);
1337
1338 let pawn_opening = [
1340 0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, 50, 50, 50, 50, 50, 10, 10, 20, 30, 30, 20, 10, 10,
1341 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,
1342 10, 10, -25, -25, 10, 10, 5, 0, 0, 0, 0, 0, 0, 0, 0,
1343 ];
1344
1345 let pawn_endgame = [
1346 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80, 50, 50, 50, 50, 50, 50, 50, 50,
1347 30, 30, 30, 30, 30, 30, 30, 30, 20, 20, 20, 20, 20, 20, 20, 20, 10, 10, 10, 10, 10, 10,
1348 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0,
1349 ];
1350
1351 let knight_opening = [
1353 -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 0, 0, 0, -20, -40, -30, 0, 10, 15,
1354 15, 10, 0, -30, -30, 5, 15, 20, 20, 15, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, -30, 5,
1355 10, 15, 15, 10, 5, -30, -40, -20, 0, 5, 5, 0, -20, -40, -50, -40, -30, -30, -30, -30,
1356 -40, -50,
1357 ];
1358
1359 let knight_endgame = [
1360 -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 5, 5, 0, -20, -40, -30, 0, 10, 15,
1361 15, 10, 0, -30, -30, 5, 15, 20, 20, 15, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, -30, 5,
1362 10, 15, 15, 10, 5, -30, -40, -20, 0, 5, 5, 0, -20, -40, -50, -40, -30, -30, -30, -30,
1363 -40, -50,
1364 ];
1365
1366 let bishop_opening = [
1368 -20, -10, -10, -10, -10, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 10, 10,
1369 5, 0, -10, -10, 5, 5, 10, 10, 5, 5, -10, -10, 0, 10, 10, 10, 10, 0, -10, -10, 10, 10,
1370 10, 10, 10, 10, -10, -10, 5, 0, 0, 0, 0, 5, -10, -20, -10, -10, -10, -10, -10, -10,
1371 -20,
1372 ];
1373
1374 let bishop_endgame = [
1375 -20, -10, -10, -10, -10, -10, -10, -20, -10, 5, 0, 0, 0, 0, 5, -10, -10, 0, 10, 15, 15,
1376 10, 0, -10, -10, 0, 15, 20, 20, 15, 0, -10, -10, 0, 15, 20, 20, 15, 0, -10, -10, 0, 10,
1377 15, 15, 10, 0, -10, -10, 5, 0, 0, 0, 0, 5, -10, -20, -10, -10, -10, -10, -10, -10, -20,
1378 ];
1379
1380 let rook_opening = [
1382 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,
1383 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,
1384 0, 0, -5, 0, 0, 0, 5, 5, 0, 0, 0,
1385 ];
1386
1387 let rook_endgame = [
1388 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,
1389 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,
1390 0, 0, 0, 0, 0, 0, 0, 0, 0,
1391 ];
1392
1393 let queen_opening = [
1395 -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, -10, 0, 5, 5, 5, 5,
1396 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,
1397 -10, 0, 5, 0, 0, 0, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20,
1398 ];
1399
1400 let queen_endgame = [
1401 -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 5, 5, 5, 5, 0, -10, -10, 5, 10, 10, 10,
1402 10, 5, -10, -5, 0, 10, 10, 10, 10, 0, -5, -5, 0, 10, 10, 10, 10, 0, -5, -10, 5, 10, 10,
1403 10, 10, 5, -10, -10, 0, 5, 5, 5, 5, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20,
1404 ];
1405
1406 let king_opening = [
1408 -30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, -30,
1409 -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, -20, -30,
1410 -30, -40, -40, -30, -30, -20, -10, -20, -20, -20, -20, -20, -20, -10, 20, 20, 0, 0, 0,
1411 0, 20, 20, 20, 30, 10, 0, 0, 10, 30, 20,
1412 ];
1413
1414 let king_endgame = [
1415 -50, -40, -30, -20, -20, -30, -40, -50, -30, -20, -10, 0, 0, -10, -20, -30, -30, -10,
1416 20, 30, 30, 20, -10, -30, -30, -10, 30, 40, 40, 30, -10, -30, -30, -10, 30, 40, 40, 30,
1417 -10, -30, -30, -10, 20, 30, 30, 20, -10, -30, -30, -30, 0, 0, 0, 0, -30, -30, -50, -30,
1418 -30, -30, -30, -30, -30, -50,
1419 ];
1420
1421 let phase_factor = match game_phase {
1423 GamePhase::Opening => 1.0,
1424 GamePhase::Middlegame => 0.5,
1425 GamePhase::Endgame => 0.0,
1426 };
1427
1428 for color in [Color::White, Color::Black] {
1430 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
1431
1432 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
1434 for square in pawns {
1435 let idx = if color == Color::White {
1436 square.to_index()
1437 } else {
1438 square.to_index() ^ 56
1439 };
1440 let opening_value = pawn_opening[idx] as f32;
1441 let endgame_value = pawn_endgame[idx] as f32;
1442 let interpolated_value =
1443 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1444 score += interpolated_value * multiplier * 0.01; }
1446
1447 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
1449 for square in knights {
1450 let idx = if color == Color::White {
1451 square.to_index()
1452 } else {
1453 square.to_index() ^ 56
1454 };
1455 let opening_value = knight_opening[idx] as f32;
1456 let endgame_value = knight_endgame[idx] as f32;
1457 let interpolated_value =
1458 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1459 score += interpolated_value * multiplier * 0.01;
1460 }
1461
1462 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
1464 for square in bishops {
1465 let idx = if color == Color::White {
1466 square.to_index()
1467 } else {
1468 square.to_index() ^ 56
1469 };
1470 let opening_value = bishop_opening[idx] as f32;
1471 let endgame_value = bishop_endgame[idx] as f32;
1472 let interpolated_value =
1473 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1474 score += interpolated_value * multiplier * 0.01;
1475 }
1476
1477 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
1479 for square in rooks {
1480 let idx = if color == Color::White {
1481 square.to_index()
1482 } else {
1483 square.to_index() ^ 56
1484 };
1485 let opening_value = rook_opening[idx] as f32;
1486 let endgame_value = rook_endgame[idx] as f32;
1487 let interpolated_value =
1488 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1489 score += interpolated_value * multiplier * 0.01;
1490 }
1491
1492 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
1494 for square in queens {
1495 let idx = if color == Color::White {
1496 square.to_index()
1497 } else {
1498 square.to_index() ^ 56
1499 };
1500 let opening_value = queen_opening[idx] as f32;
1501 let endgame_value = queen_endgame[idx] as f32;
1502 let interpolated_value =
1503 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1504 score += interpolated_value * multiplier * 0.01;
1505 }
1506
1507 let kings = board.pieces(chess::Piece::King) & board.color_combined(color);
1509 for square in kings {
1510 let idx = if color == Color::White {
1511 square.to_index()
1512 } else {
1513 square.to_index() ^ 56
1514 };
1515 let opening_value = king_opening[idx] as f32;
1516 let endgame_value = king_endgame[idx] as f32;
1517 let interpolated_value =
1518 opening_value * phase_factor + endgame_value * (1.0 - phase_factor);
1519 score += interpolated_value * multiplier * 0.01;
1520 }
1521 }
1522
1523 score
1524 }
1525
1526 fn detect_game_phase(&self, board: &Board) -> GamePhase {
1528 let mut total_material = 0;
1529
1530 for color in [Color::White, Color::Black] {
1532 total_material +=
1533 (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt() * 9;
1534 total_material +=
1535 (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt() * 5;
1536 total_material +=
1537 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt() * 3;
1538 total_material +=
1539 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt() * 3;
1540 }
1541
1542 if total_material >= 60 {
1544 GamePhase::Opening
1545 } else if total_material >= 20 {
1546 GamePhase::Middlegame
1547 } else {
1548 GamePhase::Endgame
1549 }
1550 }
1551
1552 fn mobility_evaluation(&self, board: &Board) -> f32 {
1554 let mut score = 0.0;
1555
1556 for color in [Color::White, Color::Black] {
1557 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
1558 let mobility_score = self.calculate_piece_mobility(board, color);
1559 score += mobility_score * multiplier;
1560 }
1561
1562 score
1563 }
1564
1565 fn calculate_piece_mobility(&self, board: &Board, color: Color) -> f32 {
1567 let mut mobility = 0.0;
1568
1569 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
1571 for knight_square in knights {
1572 let knight_moves = self.count_knight_moves(board, knight_square, color);
1573 mobility += knight_moves as f32 * 4.0; }
1575
1576 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
1578 for bishop_square in bishops {
1579 let bishop_moves = self.count_bishop_moves(board, bishop_square, color);
1580 mobility += bishop_moves as f32 * 3.0; }
1582
1583 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
1585 for rook_square in rooks {
1586 let rook_moves = self.count_rook_moves(board, rook_square, color);
1587 mobility += rook_moves as f32 * 2.0; }
1589
1590 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
1592 for queen_square in queens {
1593 let queen_moves = self.count_queen_moves(board, queen_square, color);
1594 mobility += queen_moves as f32 * 1.0; }
1596
1597 let pawn_mobility = self.calculate_pawn_mobility(board, color);
1599 mobility += pawn_mobility * 5.0; mobility
1602 }
1603
1604 fn count_knight_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1606 let mut count = 0;
1607 let knight_offsets = [
1608 (-2, -1),
1609 (-2, 1),
1610 (-1, -2),
1611 (-1, 2),
1612 (1, -2),
1613 (1, 2),
1614 (2, -1),
1615 (2, 1),
1616 ];
1617
1618 let file = square.get_file().to_index() as i8;
1619 let rank = square.get_rank().to_index() as i8;
1620
1621 for (df, dr) in knight_offsets {
1622 let new_file = file + df;
1623 let new_rank = rank + dr;
1624
1625 if (0..8).contains(&new_file) && (0..8).contains(&new_rank) {
1626 let dest_square = Square::make_square(
1627 chess::Rank::from_index(new_rank as usize),
1628 chess::File::from_index(new_file as usize),
1629 );
1630 if let Some(_piece_on_dest) = board.piece_on(dest_square) {
1632 if board.color_on(dest_square) != Some(color) {
1633 count += 1; }
1635 } else {
1636 count += 1; }
1638 }
1639 }
1640
1641 count
1642 }
1643
1644 fn count_bishop_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1646 let mut count = 0;
1647 let directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)];
1648
1649 for (df, dr) in directions {
1650 count += self.count_sliding_moves(board, square, color, df, dr);
1651 }
1652
1653 count
1654 }
1655
1656 fn count_rook_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1658 let mut count = 0;
1659 let directions = [(1, 0), (-1, 0), (0, 1), (0, -1)];
1660
1661 for (df, dr) in directions {
1662 count += self.count_sliding_moves(board, square, color, df, dr);
1663 }
1664
1665 count
1666 }
1667
1668 fn count_queen_moves(&self, board: &Board, square: Square, color: Color) -> usize {
1670 let mut count = 0;
1671 let directions = [
1672 (1, 0),
1673 (-1, 0),
1674 (0, 1),
1675 (0, -1), (1, 1),
1677 (1, -1),
1678 (-1, 1),
1679 (-1, -1), ];
1681
1682 for (df, dr) in directions {
1683 count += self.count_sliding_moves(board, square, color, df, dr);
1684 }
1685
1686 count
1687 }
1688
1689 fn count_sliding_moves(
1691 &self,
1692 board: &Board,
1693 square: Square,
1694 color: Color,
1695 df: i8,
1696 dr: i8,
1697 ) -> usize {
1698 let mut count = 0;
1699 let mut file = square.get_file().to_index() as i8;
1700 let mut rank = square.get_rank().to_index() as i8;
1701
1702 loop {
1703 file += df;
1704 rank += dr;
1705
1706 if !(0..8).contains(&file) || !(0..8).contains(&rank) {
1707 break;
1708 }
1709
1710 let dest_square = Square::make_square(
1711 chess::Rank::from_index(rank as usize),
1712 chess::File::from_index(file as usize),
1713 );
1714 if let Some(_piece_on_dest) = board.piece_on(dest_square) {
1715 if board.color_on(dest_square) != Some(color) {
1716 count += 1; }
1718 break; } else {
1720 count += 1; }
1722 }
1723
1724 count
1725 }
1726
1727 fn calculate_pawn_mobility(&self, board: &Board, color: Color) -> f32 {
1729 let mut mobility = 0.0;
1730 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
1731
1732 let direction = if color == Color::White { 1 } else { -1 };
1733
1734 for pawn_square in pawns {
1735 let file = pawn_square.get_file().to_index() as i8;
1736 let rank = pawn_square.get_rank().to_index() as i8;
1737
1738 let advance_rank = rank + direction;
1740 if (0..8).contains(&advance_rank) {
1741 let advance_square = Square::make_square(
1742 chess::Rank::from_index(advance_rank as usize),
1743 pawn_square.get_file(),
1744 );
1745 if board.piece_on(advance_square).is_none() {
1746 mobility += 1.0; let starting_rank = if color == Color::White { 1 } else { 6 };
1750 if rank == starting_rank {
1751 let double_advance_rank = advance_rank + direction;
1752 let double_advance_square = Square::make_square(
1753 chess::Rank::from_index(double_advance_rank as usize),
1754 pawn_square.get_file(),
1755 );
1756 if board.piece_on(double_advance_square).is_none() {
1757 mobility += 0.5; }
1759 }
1760 }
1761 }
1762
1763 for capture_file in [file - 1, file + 1] {
1765 if (0..8).contains(&capture_file) && (0..8).contains(&advance_rank) {
1766 let capture_square = Square::make_square(
1767 chess::Rank::from_index(advance_rank as usize),
1768 chess::File::from_index(capture_file as usize),
1769 );
1770 if let Some(_piece) = board.piece_on(capture_square) {
1771 if board.color_on(capture_square) != Some(color) {
1772 mobility += 2.0; }
1774 }
1775 }
1776 }
1777 }
1778
1779 mobility
1780 }
1781
1782 fn tactical_bonuses(&self, board: &Board) -> f32 {
1784 let mut bonus = 0.0;
1785
1786 bonus += self.mobility_evaluation(board);
1788
1789 let captures = self.generate_captures(board);
1791 let capture_bonus = captures.len() as f32 * 0.1;
1792
1793 bonus += self.center_control_evaluation(board);
1795
1796 if board.side_to_move() == Color::White {
1798 bonus += capture_bonus;
1799 } else {
1800 bonus -= capture_bonus;
1801 }
1802
1803 bonus
1804 }
1805
1806 fn center_control_evaluation(&self, board: &Board) -> f32 {
1808 let mut score = 0.0;
1809 let center_squares = [
1810 Square::make_square(chess::Rank::Fourth, chess::File::D),
1811 Square::make_square(chess::Rank::Fourth, chess::File::E),
1812 Square::make_square(chess::Rank::Fifth, chess::File::D),
1813 Square::make_square(chess::Rank::Fifth, chess::File::E),
1814 ];
1815
1816 let extended_center = [
1817 Square::make_square(chess::Rank::Third, chess::File::C),
1818 Square::make_square(chess::Rank::Third, chess::File::D),
1819 Square::make_square(chess::Rank::Third, chess::File::E),
1820 Square::make_square(chess::Rank::Third, chess::File::F),
1821 Square::make_square(chess::Rank::Fourth, chess::File::C),
1822 Square::make_square(chess::Rank::Fourth, chess::File::F),
1823 Square::make_square(chess::Rank::Fifth, chess::File::C),
1824 Square::make_square(chess::Rank::Fifth, chess::File::F),
1825 Square::make_square(chess::Rank::Sixth, chess::File::C),
1826 Square::make_square(chess::Rank::Sixth, chess::File::D),
1827 Square::make_square(chess::Rank::Sixth, chess::File::E),
1828 Square::make_square(chess::Rank::Sixth, chess::File::F),
1829 ];
1830
1831 for &square in ¢er_squares {
1833 if let Some(piece) = board.piece_on(square) {
1834 if piece == chess::Piece::Pawn {
1835 if let Some(color) = board.color_on(square) {
1836 let bonus = if color == Color::White { 30.0 } else { -30.0 };
1837 score += bonus;
1838 }
1839 }
1840 }
1841 }
1842
1843 for &square in &extended_center {
1845 if let Some(_piece) = board.piece_on(square) {
1846 if let Some(color) = board.color_on(square) {
1847 let bonus = if color == Color::White { 5.0 } else { -5.0 };
1848 score += bonus;
1849 }
1850 }
1851 }
1852
1853 score
1854 }
1855
1856 fn king_safety(&self, board: &Board) -> f32 {
1858 let mut safety = 0.0;
1859 let game_phase = self.detect_game_phase(board);
1860
1861 for color in [Color::White, Color::Black] {
1862 let mut king_safety = 0.0;
1863 let king_square = board.king_square(color);
1864 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
1865
1866 king_safety += self.evaluate_castling_safety(board, color, king_square, game_phase);
1868
1869 king_safety += self.evaluate_pawn_shield(board, color, king_square, game_phase);
1871
1872 king_safety += self.evaluate_king_attackers(board, color, king_square);
1874
1875 king_safety += self.evaluate_open_lines_near_king(board, color, king_square);
1877
1878 if game_phase == GamePhase::Endgame {
1880 king_safety += self.evaluate_king_endgame_activity(board, color, king_square);
1881 }
1882
1883 king_safety += self.evaluate_king_zone_control(board, color, king_square);
1885
1886 if board.checkers().popcnt() > 0 && board.side_to_move() == color {
1888 let check_severity = self.evaluate_check_severity(board, color);
1889 king_safety -= check_severity;
1890 }
1891
1892 safety += king_safety * multiplier;
1893 }
1894
1895 safety
1896 }
1897
1898 fn evaluate_castling_safety(
1900 &self,
1901 board: &Board,
1902 color: Color,
1903 king_square: Square,
1904 game_phase: GamePhase,
1905 ) -> f32 {
1906 let mut score = 0.0;
1907
1908 let starting_square = if color == Color::White {
1909 Square::E1
1910 } else {
1911 Square::E8
1912 };
1913 let kingside_castle = if color == Color::White {
1914 Square::G1
1915 } else {
1916 Square::G8
1917 };
1918 let queenside_castle = if color == Color::White {
1919 Square::C1
1920 } else {
1921 Square::C8
1922 };
1923
1924 match game_phase {
1925 GamePhase::Opening | GamePhase::Middlegame => {
1926 if king_square == kingside_castle {
1927 score += 50.0; } else if king_square == queenside_castle {
1929 score += 35.0; } else if king_square == starting_square {
1931 let castle_rights = board.castle_rights(color);
1933 if castle_rights.has_kingside() {
1934 score += 25.0;
1935 }
1936 if castle_rights.has_queenside() {
1937 score += 15.0;
1938 }
1939 } else {
1940 score -= 80.0;
1942 }
1943 }
1944 GamePhase::Endgame => {
1945 let rank = king_square.get_rank().to_index() as i8;
1947 let file = king_square.get_file().to_index() as i8;
1948 let center_distance = (rank as f32 - 3.5).abs() + (file as f32 - 3.5).abs();
1949 score += (7.0 - center_distance) * 5.0; }
1951 }
1952
1953 score
1954 }
1955
1956 fn evaluate_pawn_shield(
1958 &self,
1959 board: &Board,
1960 color: Color,
1961 king_square: Square,
1962 game_phase: GamePhase,
1963 ) -> f32 {
1964 if game_phase == GamePhase::Endgame {
1965 return 0.0; }
1967
1968 let mut shield_score = 0.0;
1969 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
1970 let king_file = king_square.get_file().to_index() as i8;
1971 let king_rank = king_square.get_rank().to_index() as i8;
1972
1973 let shield_files = [king_file - 1, king_file, king_file + 1];
1975 let forward_direction = if color == Color::White { 1 } else { -1 };
1976
1977 for &file in &shield_files {
1978 if (0..8).contains(&file) {
1979 let mut found_pawn = false;
1980 let file_mask = self.get_file_mask(chess::File::from_index(file as usize));
1981 let file_pawns = pawns & file_mask;
1982
1983 for pawn_square in file_pawns {
1984 let pawn_rank = pawn_square.get_rank().to_index() as i8;
1985 let rank_distance = (pawn_rank - king_rank) * forward_direction;
1986
1987 if rank_distance > 0 && rank_distance <= 3 {
1988 found_pawn = true;
1989 let protection_value = match rank_distance {
1991 1 => 25.0, 2 => 15.0, 3 => 8.0, _ => 0.0,
1995 };
1996 shield_score += protection_value;
1997 break;
1998 }
1999 }
2000
2001 if !found_pawn {
2003 shield_score -= 20.0;
2004 }
2005 }
2006 }
2007
2008 let is_kingside = king_file >= 6;
2010 let is_queenside = king_file <= 2;
2011
2012 if is_kingside {
2013 shield_score += self.evaluate_kingside_pawn_structure(board, color);
2014 } else if is_queenside {
2015 shield_score += self.evaluate_queenside_pawn_structure(board, color);
2016 }
2017
2018 shield_score
2019 }
2020
2021 fn evaluate_kingside_pawn_structure(&self, board: &Board, color: Color) -> f32 {
2023 let mut score = 0.0;
2024 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2025 let base_rank = if color == Color::White { 1 } else { 6 };
2026
2027 for (file_idx, ideal_rank) in [(5, base_rank), (6, base_rank), (7, base_rank)] {
2029 let file_mask = self.get_file_mask(chess::File::from_index(file_idx));
2030 let file_pawns = pawns & file_mask;
2031
2032 let mut found_intact = false;
2033 for pawn_square in file_pawns {
2034 if pawn_square.get_rank().to_index() == ideal_rank {
2035 found_intact = true;
2036 score += 10.0; break;
2038 }
2039 }
2040
2041 if !found_intact {
2042 score -= 15.0; }
2044 }
2045
2046 score
2047 }
2048
2049 fn evaluate_queenside_pawn_structure(&self, board: &Board, color: Color) -> f32 {
2051 let mut score = 0.0;
2052 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2053 let base_rank = if color == Color::White { 1 } else { 6 };
2054
2055 for (file_idx, ideal_rank) in [(0, base_rank), (1, base_rank), (2, base_rank)] {
2057 let file_mask = self.get_file_mask(chess::File::from_index(file_idx));
2058 let file_pawns = pawns & file_mask;
2059
2060 let mut found_intact = false;
2061 for pawn_square in file_pawns {
2062 if pawn_square.get_rank().to_index() == ideal_rank {
2063 found_intact = true;
2064 score += 8.0; break;
2066 }
2067 }
2068
2069 if !found_intact {
2070 score -= 12.0; }
2072 }
2073
2074 score
2075 }
2076
2077 fn evaluate_king_attackers(&self, board: &Board, color: Color, king_square: Square) -> f32 {
2079 let mut attack_score = 0.0;
2080 let enemy_color = !color;
2081
2082 let enemy_queens = board.pieces(chess::Piece::Queen) & board.color_combined(enemy_color);
2084 let enemy_rooks = board.pieces(chess::Piece::Rook) & board.color_combined(enemy_color);
2085 let enemy_bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(enemy_color);
2086 let enemy_knights = board.pieces(chess::Piece::Knight) & board.color_combined(enemy_color);
2087
2088 for queen_square in enemy_queens {
2090 if self.can_attack_square(board, queen_square, king_square, chess::Piece::Queen) {
2091 attack_score -= 50.0;
2092 }
2093 }
2094
2095 for rook_square in enemy_rooks {
2097 if self.can_attack_square(board, rook_square, king_square, chess::Piece::Rook) {
2098 attack_score -= 30.0;
2099 }
2100 }
2101
2102 for bishop_square in enemy_bishops {
2104 if self.can_attack_square(board, bishop_square, king_square, chess::Piece::Bishop) {
2105 attack_score -= 25.0;
2106 }
2107 }
2108
2109 for knight_square in enemy_knights {
2111 if self.can_attack_square(board, knight_square, king_square, chess::Piece::Knight) {
2112 attack_score -= 20.0;
2113 }
2114 }
2115
2116 attack_score
2117 }
2118
2119 fn can_attack_square(
2121 &self,
2122 board: &Board,
2123 piece_square: Square,
2124 target_square: Square,
2125 piece_type: chess::Piece,
2126 ) -> bool {
2127 match piece_type {
2128 chess::Piece::Queen | chess::Piece::Rook | chess::Piece::Bishop => {
2129 self.has_clear_line_of_attack(board, piece_square, target_square, piece_type)
2131 }
2132 chess::Piece::Knight => {
2133 let file_diff = (piece_square.get_file().to_index() as i8
2135 - target_square.get_file().to_index() as i8)
2136 .abs();
2137 let rank_diff = (piece_square.get_rank().to_index() as i8
2138 - target_square.get_rank().to_index() as i8)
2139 .abs();
2140 (file_diff == 2 && rank_diff == 1) || (file_diff == 1 && rank_diff == 2)
2141 }
2142 _ => false,
2143 }
2144 }
2145
2146 fn has_clear_line_of_attack(
2148 &self,
2149 board: &Board,
2150 from: Square,
2151 to: Square,
2152 piece_type: chess::Piece,
2153 ) -> bool {
2154 let from_file = from.get_file().to_index() as i8;
2155 let from_rank = from.get_rank().to_index() as i8;
2156 let to_file = to.get_file().to_index() as i8;
2157 let to_rank = to.get_rank().to_index() as i8;
2158
2159 let file_diff = to_file - from_file;
2160 let rank_diff = to_rank - from_rank;
2161
2162 let is_valid_attack = match piece_type {
2164 chess::Piece::Rook | chess::Piece::Queen => {
2165 file_diff == 0 || rank_diff == 0 || file_diff.abs() == rank_diff.abs()
2166 }
2167 chess::Piece::Bishop => file_diff.abs() == rank_diff.abs(),
2168 _ => false,
2169 };
2170
2171 if !is_valid_attack {
2172 return false;
2173 }
2174
2175 let file_step = if file_diff == 0 {
2177 0
2178 } else {
2179 file_diff.signum()
2180 };
2181 let rank_step = if rank_diff == 0 {
2182 0
2183 } else {
2184 rank_diff.signum()
2185 };
2186
2187 let mut current_file = from_file + file_step;
2188 let mut current_rank = from_rank + rank_step;
2189
2190 while current_file != to_file || current_rank != to_rank {
2191 let square = Square::make_square(
2192 chess::Rank::from_index(current_rank as usize),
2193 chess::File::from_index(current_file as usize),
2194 );
2195 if board.piece_on(square).is_some() {
2196 return false; }
2198 current_file += file_step;
2199 current_rank += rank_step;
2200 }
2201
2202 true
2203 }
2204
2205 fn evaluate_open_lines_near_king(
2207 &self,
2208 board: &Board,
2209 color: Color,
2210 king_square: Square,
2211 ) -> f32 {
2212 let mut line_score = 0.0;
2213 let king_file = king_square.get_file();
2214 let _king_rank = king_square.get_rank();
2215
2216 for file_offset in -1..=1i8 {
2218 let file_index = (king_file.to_index() as i8 + file_offset).clamp(0, 7) as usize;
2219 let file = chess::File::from_index(file_index);
2220 if self.is_open_file(board, file) {
2221 line_score -= 20.0; } else if self.is_semi_open_file(board, file, color) {
2223 line_score -= 10.0; }
2225 }
2226
2227 line_score += self.evaluate_diagonal_safety(board, color, king_square);
2229
2230 line_score
2231 }
2232
2233 fn is_open_file(&self, board: &Board, file: chess::File) -> bool {
2235 let file_mask = self.get_file_mask(file);
2236 let all_pawns = board.pieces(chess::Piece::Pawn);
2237 (all_pawns & file_mask).popcnt() == 0
2238 }
2239
2240 fn is_semi_open_file(&self, board: &Board, file: chess::File, color: Color) -> bool {
2242 let file_mask = self.get_file_mask(file);
2243 let own_pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2244 (own_pawns & file_mask).popcnt() == 0
2245 }
2246
2247 fn evaluate_diagonal_safety(&self, board: &Board, color: Color, king_square: Square) -> f32 {
2249 let mut score = 0.0;
2250 let enemy_color = !color;
2251 let enemy_bishops_queens = (board.pieces(chess::Piece::Bishop)
2252 | board.pieces(chess::Piece::Queen))
2253 & board.color_combined(enemy_color);
2254
2255 let directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)];
2257
2258 for (file_dir, rank_dir) in directions {
2259 if self.has_diagonal_threat(
2260 board,
2261 king_square,
2262 file_dir,
2263 rank_dir,
2264 enemy_bishops_queens,
2265 ) {
2266 score -= 15.0; }
2268 }
2269
2270 score
2271 }
2272
2273 fn has_diagonal_threat(
2275 &self,
2276 board: &Board,
2277 king_square: Square,
2278 file_dir: i8,
2279 rank_dir: i8,
2280 enemy_pieces: chess::BitBoard,
2281 ) -> bool {
2282 let mut file = king_square.get_file().to_index() as i8 + file_dir;
2283 let mut rank = king_square.get_rank().to_index() as i8 + rank_dir;
2284
2285 while (0..8).contains(&file) && (0..8).contains(&rank) {
2286 let square = Square::make_square(
2287 chess::Rank::from_index(rank as usize),
2288 chess::File::from_index(file as usize),
2289 );
2290 if let Some(_piece) = board.piece_on(square) {
2291 return (enemy_pieces & chess::BitBoard::from_square(square)).popcnt() > 0;
2293 }
2294 file += file_dir;
2295 rank += rank_dir;
2296 }
2297
2298 false
2299 }
2300
2301 fn evaluate_king_endgame_activity(
2303 &self,
2304 board: &Board,
2305 color: Color,
2306 king_square: Square,
2307 ) -> f32 {
2308 let mut activity_score = 0.0;
2309
2310 let file = king_square.get_file().to_index() as f32;
2312 let rank = king_square.get_rank().to_index() as f32;
2313 let center_distance = ((file - 3.5).abs() + (rank - 3.5).abs()) / 2.0;
2314 activity_score += (3.5 - center_distance) * 10.0;
2315
2316 let enemy_pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(!color);
2318 for enemy_pawn in enemy_pawns {
2319 let distance = ((king_square.get_file().to_index() as i8
2320 - enemy_pawn.get_file().to_index() as i8)
2321 .abs()
2322 + (king_square.get_rank().to_index() as i8
2323 - enemy_pawn.get_rank().to_index() as i8)
2324 .abs()) as f32;
2325 if distance <= 3.0 {
2326 activity_score += 5.0; }
2328 }
2329
2330 activity_score
2331 }
2332
2333 fn evaluate_king_zone_control(&self, board: &Board, color: Color, king_square: Square) -> f32 {
2335 let mut control_score = 0.0;
2336 let king_file = king_square.get_file().to_index() as i8;
2337 let king_rank = king_square.get_rank().to_index() as i8;
2338
2339 for file_offset in -1..=1 {
2341 for rank_offset in -1..=1 {
2342 if file_offset == 0 && rank_offset == 0 {
2343 continue; }
2345
2346 let check_file = king_file + file_offset;
2347 let check_rank = king_rank + rank_offset;
2348
2349 if (0..8).contains(&check_file) && (0..8).contains(&check_rank) {
2350 let square = Square::make_square(
2351 chess::Rank::from_index(check_rank as usize),
2352 chess::File::from_index(check_file as usize),
2353 );
2354 if let Some(_piece) = board.piece_on(square) {
2355 if board.color_on(square) == Some(color) {
2356 control_score += 3.0; } else {
2358 control_score -= 5.0; }
2360 }
2361 }
2362 }
2363 }
2364
2365 control_score
2366 }
2367
2368 fn evaluate_check_severity(&self, board: &Board, _color: Color) -> f32 {
2370 let checkers = board.checkers();
2371 let check_count = checkers.popcnt();
2372
2373 let base_penalty = match check_count {
2374 0 => 0.0,
2375 1 => 50.0, 2 => 150.0, _ => 200.0, };
2379
2380 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
2382 let king_moves = legal_moves
2383 .iter()
2384 .filter(|mv| board.piece_on(mv.get_source()) == Some(chess::Piece::King))
2385 .count();
2386
2387 let escape_penalty = match king_moves {
2388 0 => 100.0, 1 => 30.0, 2 => 15.0, _ => 0.0, };
2393
2394 base_penalty + escape_penalty
2395 }
2396
2397 fn determine_game_phase(&self, board: &Board) -> GamePhase {
2399 let mut material_count = 0;
2401
2402 for piece in [
2403 chess::Piece::Queen,
2404 chess::Piece::Rook,
2405 chess::Piece::Bishop,
2406 chess::Piece::Knight,
2407 ] {
2408 material_count += board.pieces(piece).popcnt();
2409 }
2410
2411 match material_count {
2412 0..=4 => GamePhase::Endgame, 5..=12 => GamePhase::Middlegame, _ => GamePhase::Opening, }
2416 }
2417
2418 #[allow(dead_code)]
2420 fn count_king_attackers(&self, board: &Board, color: Color) -> u32 {
2421 let king_square = board.king_square(color);
2422 let opponent_color = if color == Color::White {
2423 Color::Black
2424 } else {
2425 Color::White
2426 };
2427
2428 let mut attackers = 0;
2430
2431 for piece in [
2433 chess::Piece::Queen,
2434 chess::Piece::Rook,
2435 chess::Piece::Bishop,
2436 chess::Piece::Knight,
2437 chess::Piece::Pawn,
2438 ] {
2439 let enemy_pieces = board.pieces(piece) & board.color_combined(opponent_color);
2440
2441 for square in enemy_pieces {
2443 let rank_diff = (king_square.get_rank().to_index() as i32
2444 - square.get_rank().to_index() as i32)
2445 .abs();
2446 let file_diff = (king_square.get_file().to_index() as i32
2447 - square.get_file().to_index() as i32)
2448 .abs();
2449
2450 let is_threat = match piece {
2452 chess::Piece::Queen => rank_diff <= 2 || file_diff <= 2,
2453 chess::Piece::Rook => rank_diff <= 2 || file_diff <= 2,
2454 chess::Piece::Bishop => rank_diff == file_diff && rank_diff <= 2,
2455 chess::Piece::Knight => {
2456 (rank_diff == 2 && file_diff == 1) || (rank_diff == 1 && file_diff == 2)
2457 }
2458 chess::Piece::Pawn => {
2459 rank_diff == 1
2460 && file_diff == 1
2461 && ((color == Color::White
2462 && square.get_rank().to_index()
2463 > king_square.get_rank().to_index())
2464 || (color == Color::Black
2465 && square.get_rank().to_index()
2466 < king_square.get_rank().to_index()))
2467 }
2468 _ => false,
2469 };
2470
2471 if is_threat {
2472 attackers += 1;
2473 }
2474 }
2475 }
2476
2477 attackers
2478 }
2479
2480 fn get_file_mask(&self, file: chess::File) -> chess::BitBoard {
2482 chess::BitBoard(0x0101010101010101u64 << file.to_index())
2483 }
2484
2485 fn evaluate_pawn_structure(&self, board: &Board) -> f32 {
2487 let mut score = 0.0;
2488
2489 for color in [Color::White, Color::Black] {
2490 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
2491 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2492
2493 for file in 0..8 {
2495 let file_mask = self.get_file_mask(chess::File::from_index(file));
2496 let file_pawns = pawns & file_mask;
2497 let pawn_count = file_pawns.popcnt();
2498
2499 if pawn_count > 1 {
2501 score += -0.5 * multiplier * (pawn_count - 1) as f32; }
2503
2504 if pawn_count > 0 {
2506 let has_adjacent_pawns = self.has_adjacent_pawns(board, color, file);
2507 if !has_adjacent_pawns {
2508 score += -0.3 * multiplier; }
2510 }
2511
2512 for square in file_pawns {
2514 if self.is_passed_pawn(board, square, color) {
2516 let rank = square.get_rank().to_index();
2517 let advancement = if color == Color::White {
2518 rank
2519 } else {
2520 7 - rank
2521 };
2522 score += (0.2 + advancement as f32 * 0.3) * multiplier; }
2524
2525 if self.is_backward_pawn(board, square, color) {
2527 score += -0.2 * multiplier;
2528 }
2529
2530 if self.has_pawn_support(board, square, color) {
2532 score += 0.1 * multiplier;
2533 }
2534 }
2535 }
2536
2537 score += self.evaluate_pawn_chains(board, color) * multiplier;
2539 }
2540
2541 score
2542 }
2543
2544 fn has_adjacent_pawns(&self, board: &Board, color: Color, file: usize) -> bool {
2546 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2547
2548 if file > 0 {
2550 let left_file_mask = self.get_file_mask(chess::File::from_index(file - 1));
2551 if (pawns & left_file_mask).popcnt() > 0 {
2552 return true;
2553 }
2554 }
2555
2556 if file < 7 {
2557 let right_file_mask = self.get_file_mask(chess::File::from_index(file + 1));
2558 if (pawns & right_file_mask).popcnt() > 0 {
2559 return true;
2560 }
2561 }
2562
2563 false
2564 }
2565
2566 fn is_passed_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
2568 let opponent_color = if color == Color::White {
2569 Color::Black
2570 } else {
2571 Color::White
2572 };
2573 let opponent_pawns =
2574 board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color);
2575
2576 let file = pawn_square.get_file().to_index();
2577 let rank = pawn_square.get_rank().to_index();
2578
2579 for opponent_square in opponent_pawns {
2581 let opp_file = opponent_square.get_file().to_index();
2582 let opp_rank = opponent_square.get_rank().to_index();
2583
2584 let file_diff = (file as i32 - opp_file as i32).abs();
2586
2587 if file_diff <= 1 {
2588 if color == Color::White && opp_rank > rank {
2590 return false; }
2592 if color == Color::Black && opp_rank < rank {
2593 return false; }
2595 }
2596 }
2597
2598 true
2599 }
2600
2601 fn is_backward_pawn(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
2603 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2604 let file = pawn_square.get_file().to_index();
2605 let rank = pawn_square.get_rank().to_index();
2606
2607 for support_file in [file.saturating_sub(1), (file + 1).min(7)] {
2609 if support_file == file {
2610 continue;
2611 }
2612
2613 let file_mask = self.get_file_mask(chess::File::from_index(support_file));
2614 let file_pawns = pawns & file_mask;
2615
2616 for support_square in file_pawns {
2617 let support_rank = support_square.get_rank().to_index();
2618
2619 if color == Color::White && support_rank < rank {
2621 return false; }
2623 if color == Color::Black && support_rank > rank {
2624 return false; }
2626 }
2627 }
2628
2629 true
2630 }
2631
2632 fn has_pawn_support(&self, board: &Board, pawn_square: Square, color: Color) -> bool {
2634 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2635 let file = pawn_square.get_file().to_index();
2636 let rank = pawn_square.get_rank().to_index();
2637
2638 for support_file in [file.saturating_sub(1), (file + 1).min(7)] {
2640 if support_file == file {
2641 continue;
2642 }
2643
2644 let file_mask = self.get_file_mask(chess::File::from_index(support_file));
2645 let file_pawns = pawns & file_mask;
2646
2647 for support_square in file_pawns {
2648 let support_rank = support_square.get_rank().to_index();
2649
2650 if (support_rank as i32 - rank as i32).abs() == 1 {
2652 return true;
2653 }
2654 }
2655 }
2656
2657 false
2658 }
2659
2660 fn evaluate_pawn_chains(&self, board: &Board, color: Color) -> f32 {
2662 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2663 let mut chain_score = 0.0;
2664
2665 let mut chain_lengths = Vec::new();
2667 let mut visited = std::collections::HashSet::new();
2668
2669 for pawn_square in pawns {
2670 if visited.contains(&pawn_square) {
2671 continue;
2672 }
2673
2674 let chain_length = self.count_pawn_chain(board, pawn_square, color, &mut visited);
2675 if chain_length > 1 {
2676 chain_lengths.push(chain_length);
2677 }
2678 }
2679
2680 for &length in &chain_lengths {
2682 chain_score += (length as f32 - 1.0) * 0.15; }
2684
2685 chain_score
2686 }
2687
2688 #[allow(clippy::only_used_in_recursion)]
2690 fn count_pawn_chain(
2691 &self,
2692 board: &Board,
2693 start_square: Square,
2694 color: Color,
2695 visited: &mut std::collections::HashSet<Square>,
2696 ) -> usize {
2697 if visited.contains(&start_square) {
2698 return 0;
2699 }
2700
2701 visited.insert(start_square);
2702 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2703
2704 if (pawns & chess::BitBoard::from_square(start_square)) == chess::BitBoard(0) {
2706 return 0;
2707 }
2708
2709 let mut count = 1;
2710 let file = start_square.get_file().to_index();
2711 let rank = start_square.get_rank().to_index();
2712
2713 for &(file_offset, rank_offset) in &[(-1i32, -1i32), (-1, 1), (1, -1), (1, 1)] {
2715 let new_file = file as i32 + file_offset;
2716 let new_rank = rank as i32 + rank_offset;
2717
2718 if (0..8).contains(&new_file) && (0..8).contains(&new_rank) {
2719 let square_index = (new_rank * 8 + new_file) as u8;
2720 let new_square = unsafe { Square::new(square_index) };
2721 if (pawns & chess::BitBoard::from_square(new_square)) != chess::BitBoard(0)
2722 && !visited.contains(&new_square)
2723 {
2724 count += self.count_pawn_chain(board, new_square, color, visited);
2725 }
2726 }
2727 }
2728
2729 count
2730 }
2731
2732 fn is_tactical_position(&self, board: &Board) -> bool {
2734 if board.checkers().popcnt() > 0 {
2736 return true;
2737 }
2738
2739 let captures = self.generate_captures(board);
2741 if !captures.is_empty() {
2742 return true;
2743 }
2744
2745 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
2747 if legal_moves.len() > 35 {
2748 return true;
2749 }
2750
2751 false
2752 }
2753
2754 fn is_capture_or_promotion(&self, chess_move: &ChessMove, board: &Board) -> bool {
2756 board.piece_on(chess_move.get_dest()).is_some() || chess_move.get_promotion().is_some()
2757 }
2758
2759 fn has_non_pawn_material(&self, board: &Board, color: Color) -> bool {
2761 let pieces = board.color_combined(color)
2762 & !board.pieces(chess::Piece::Pawn)
2763 & !board.pieces(chess::Piece::King);
2764 pieces.popcnt() > 0
2765 }
2766
2767 fn is_killer_move(&self, chess_move: &ChessMove) -> bool {
2769 for depth_killers in &self.killer_moves {
2771 for killer_move in depth_killers.iter().flatten() {
2772 if killer_move == chess_move {
2773 return true;
2774 }
2775 }
2776 }
2777 false
2778 }
2779
2780 fn store_killer_move(&mut self, chess_move: ChessMove, depth: u32) {
2782 let depth_idx = (depth as usize).min(self.killer_moves.len() - 1);
2783
2784 if let Some(first_killer) = self.killer_moves[depth_idx][0] {
2786 if first_killer != chess_move {
2787 self.killer_moves[depth_idx][1] = Some(first_killer);
2788 self.killer_moves[depth_idx][0] = Some(chess_move);
2789 }
2790 } else {
2791 self.killer_moves[depth_idx][0] = Some(chess_move);
2792 }
2793 }
2794
2795 fn update_history(&mut self, chess_move: &ChessMove, depth: u32) {
2797 let key = (chess_move.get_source(), chess_move.get_dest());
2798 let bonus = depth * depth; let current = self.history_heuristic.get(&key).unwrap_or(&0);
2801 self.history_heuristic.insert(key, current + bonus);
2802 }
2803
2804 fn get_history_score(&self, chess_move: &ChessMove) -> u32 {
2806 let key = (chess_move.get_source(), chess_move.get_dest());
2807 *self.history_heuristic.get(&key).unwrap_or(&0)
2808 }
2809
2810 pub fn clear_cache(&mut self) {
2812 self.transposition_table.clear();
2813 }
2814
2815 pub fn get_stats(&self) -> (u64, usize) {
2817 (self.nodes_searched, self.transposition_table.len())
2818 }
2819
2820 fn evaluate_endgame_patterns(&self, board: &Board) -> f32 {
2822 let mut score = 0.0;
2823
2824 let piece_count = self.count_all_pieces(board);
2826 if piece_count > 10 {
2827 return 0.0; }
2829
2830 let endgame_weight = self.config.endgame_evaluation_weight;
2832
2833 score += self.evaluate_king_pawn_endgames(board) * endgame_weight;
2835 score += self.evaluate_basic_mate_patterns(board) * endgame_weight;
2836 score += self.evaluate_opposition_patterns(board) * endgame_weight;
2837 score += self.evaluate_key_squares(board) * endgame_weight;
2838 score += self.evaluate_zugzwang_patterns(board) * endgame_weight;
2839
2840 score += self.evaluate_piece_coordination_endgame(board) * endgame_weight;
2842 score += self.evaluate_fortress_patterns(board) * endgame_weight;
2843 score += self.evaluate_theoretical_endgames(board) * endgame_weight;
2844
2845 score
2846 }
2847
2848 fn count_all_pieces(&self, board: &Board) -> u32 {
2850 let mut count = 0;
2851 for piece in [
2852 chess::Piece::Pawn,
2853 chess::Piece::Knight,
2854 chess::Piece::Bishop,
2855 chess::Piece::Rook,
2856 chess::Piece::Queen,
2857 ] {
2858 count += board.pieces(piece).popcnt();
2859 }
2860 count += board.pieces(chess::Piece::King).popcnt(); count
2862 }
2863
2864 fn evaluate_king_pawn_endgames(&self, board: &Board) -> f32 {
2866 let mut score = 0.0;
2867
2868 for color in [Color::White, Color::Black] {
2870 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
2871 let king_square = board.king_square(color);
2872 let opponent_king_square = board.king_square(!color);
2873 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
2874
2875 for pawn_square in pawns {
2876 if self.is_passed_pawn(board, pawn_square, color) {
2877 let _pawn_file = pawn_square.get_file().to_index();
2878 let pawn_rank = pawn_square.get_rank().to_index();
2879
2880 let promotion_rank = if color == Color::White { 7 } else { 0 };
2882 let promotion_square = Square::make_square(
2883 chess::Rank::from_index(promotion_rank),
2884 chess::File::from_index(_pawn_file),
2885 );
2886
2887 let king_distance = self.square_distance(king_square, promotion_square);
2889 let opponent_king_distance =
2890 self.square_distance(opponent_king_square, promotion_square);
2891 let pawn_distance = (promotion_rank as i32 - pawn_rank as i32).unsigned_abs();
2892
2893 if pawn_distance < opponent_king_distance {
2895 score += 2.0 * multiplier; } else if king_distance < opponent_king_distance {
2897 score += 1.0 * multiplier; }
2899 }
2900 }
2901 }
2902
2903 score
2904 }
2905
2906 fn evaluate_basic_mate_patterns(&self, board: &Board) -> f32 {
2908 let mut score = 0.0;
2909
2910 for color in [Color::White, Color::Black] {
2911 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
2912 let opponent_color = !color;
2913
2914 let queens = (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt();
2915 let rooks = (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt();
2916 let bishops =
2917 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt();
2918 let knights =
2919 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt();
2920
2921 let opp_queens =
2922 (board.pieces(chess::Piece::Queen) & board.color_combined(opponent_color)).popcnt();
2923 let opp_rooks =
2924 (board.pieces(chess::Piece::Rook) & board.color_combined(opponent_color)).popcnt();
2925 let opp_bishops = (board.pieces(chess::Piece::Bishop)
2926 & board.color_combined(opponent_color))
2927 .popcnt();
2928 let opp_knights = (board.pieces(chess::Piece::Knight)
2929 & board.color_combined(opponent_color))
2930 .popcnt();
2931 let opp_pawns =
2932 (board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color)).popcnt();
2933
2934 if opp_queens == 0
2936 && opp_rooks == 0
2937 && opp_bishops == 0
2938 && opp_knights == 0
2939 && opp_pawns == 0
2940 {
2941 if queens > 0 || rooks > 0 {
2943 let king_square = board.king_square(color);
2945 let opponent_king_square = board.king_square(opponent_color);
2946 let corner_distance = self.distance_to_nearest_corner(opponent_king_square);
2947 let king_distance = self.square_distance(king_square, opponent_king_square);
2948
2949 score += 1.0 * multiplier; score += (7.0 - corner_distance as f32) * 0.1 * multiplier; score += (8.0 - king_distance as f32) * 0.05 * multiplier; }
2953
2954 if bishops >= 2 {
2955 let opponent_king_square = board.king_square(opponent_color);
2957 let corner_distance = self.distance_to_nearest_corner(opponent_king_square);
2958 score += 0.8 * multiplier; score += (7.0 - corner_distance as f32) * 0.08 * multiplier;
2960 }
2961
2962 if bishops >= 1 && knights >= 1 {
2963 score += 0.6 * multiplier; }
2966 }
2967 }
2968
2969 score
2970 }
2971
2972 fn evaluate_opposition_patterns(&self, board: &Board) -> f32 {
2974 let mut score = 0.0;
2975
2976 let white_king = board.king_square(Color::White);
2977 let black_king = board.king_square(Color::Black);
2978
2979 let file_diff = (white_king.get_file().to_index() as i32
2980 - black_king.get_file().to_index() as i32)
2981 .abs();
2982 let rank_diff = (white_king.get_rank().to_index() as i32
2983 - black_king.get_rank().to_index() as i32)
2984 .abs();
2985
2986 if (file_diff == 0 && rank_diff == 2) || (file_diff == 2 && rank_diff == 0) {
2988 let opposition_bonus = 0.2;
2990 if board.side_to_move() == Color::White {
2991 score -= opposition_bonus; } else {
2993 score += opposition_bonus; }
2995 }
2996
2997 if file_diff == 0 && rank_diff % 2 == 0 && rank_diff > 2 {
2999 let distant_opposition_bonus = 0.1;
3000 if board.side_to_move() == Color::White {
3001 score -= distant_opposition_bonus;
3002 } else {
3003 score += distant_opposition_bonus;
3004 }
3005 }
3006
3007 score
3008 }
3009
3010 fn evaluate_key_squares(&self, board: &Board) -> f32 {
3012 let mut score = 0.0;
3013
3014 for color in [Color::White, Color::Black] {
3016 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3017 let king_square = board.king_square(color);
3018 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3019
3020 for pawn_square in pawns {
3021 if self.is_passed_pawn(board, pawn_square, color) {
3022 let key_squares = self.get_key_squares(pawn_square, color);
3024
3025 for key_square in key_squares {
3026 let distance = self.square_distance(king_square, key_square);
3027 if distance <= 1 {
3028 score += 0.3 * multiplier; } else if distance <= 2 {
3030 score += 0.1 * multiplier; }
3032 }
3033 }
3034 }
3035 }
3036
3037 score
3038 }
3039
3040 fn evaluate_zugzwang_patterns(&self, board: &Board) -> f32 {
3042 let mut score = 0.0;
3043
3044 let piece_count = self.count_all_pieces(board);
3046 if piece_count <= 6 {
3047 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
3049
3050 if legal_moves.len() <= 3 {
3052 let current_eval = self.quick_evaluate_position(board);
3054 let mut bad_moves = 0;
3055
3056 for chess_move in legal_moves.iter().take(3) {
3057 let new_board = board.make_move_new(*chess_move);
3058 let new_eval = -self.quick_evaluate_position(&new_board); if new_eval < current_eval - 0.5 {
3061 bad_moves += 1;
3062 }
3063 }
3064
3065 if bad_moves >= legal_moves.len() / 2 {
3067 let zugzwang_penalty = 0.3;
3068 if board.side_to_move() == Color::White {
3069 score -= zugzwang_penalty;
3070 } else {
3071 score += zugzwang_penalty;
3072 }
3073 }
3074 }
3075 }
3076
3077 score
3078 }
3079
3080 fn square_distance(&self, sq1: Square, sq2: Square) -> u32 {
3082 let file1 = sq1.get_file().to_index() as i32;
3083 let rank1 = sq1.get_rank().to_index() as i32;
3084 let file2 = sq2.get_file().to_index() as i32;
3085 let rank2 = sq2.get_rank().to_index() as i32;
3086
3087 ((file1 - file2).abs() + (rank1 - rank2).abs()) as u32
3088 }
3089
3090 fn distance_to_nearest_corner(&self, square: Square) -> u32 {
3092 let file = square.get_file().to_index() as i32;
3093 let rank = square.get_rank().to_index() as i32;
3094
3095 let corner_distances = [
3096 file + rank, (7 - file) + rank, file + (7 - rank), (7 - file) + (7 - rank), ];
3101
3102 *corner_distances.iter().min().unwrap() as u32
3103 }
3104
3105 fn get_key_squares(&self, pawn_square: Square, color: Color) -> Vec<Square> {
3107 let mut key_squares = Vec::new();
3108 let file = pawn_square.get_file().to_index();
3109 let rank = pawn_square.get_rank().to_index();
3110
3111 let key_rank = if color == Color::White {
3113 if rank + 2 <= 7 {
3114 rank + 2
3115 } else {
3116 return key_squares;
3117 }
3118 } else if rank >= 2 {
3119 rank - 2
3120 } else {
3121 return key_squares;
3122 };
3123
3124 for key_file in (file.saturating_sub(1))..=(file + 1).min(7) {
3126 let square = Square::make_square(
3127 chess::Rank::from_index(key_rank),
3128 chess::File::from_index(key_file),
3129 );
3130 key_squares.push(square);
3131 }
3132
3133 key_squares
3134 }
3135
3136 fn quick_evaluate_position(&self, board: &Board) -> f32 {
3138 let mut score = 0.0;
3139
3140 score += self.material_balance(board);
3142
3143 for color in [Color::White, Color::Black] {
3145 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3146 let king_square = board.king_square(color);
3147 let file = king_square.get_file().to_index();
3148 let rank = king_square.get_rank().to_index();
3149
3150 let center_distance = (file as f32 - 3.5).abs() + (rank as f32 - 3.5).abs();
3152 score += (7.0 - center_distance) * 0.05 * multiplier;
3153 }
3154
3155 score
3156 }
3157
3158 fn evaluate_piece_coordination_endgame(&self, board: &Board) -> f32 {
3160 let mut score = 0.0;
3161
3162 for color in [Color::White, Color::Black] {
3163 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3164 let king_square = board.king_square(color);
3165
3166 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3168 for rook_square in rooks {
3169 let distance = self.square_distance(king_square, rook_square);
3170 if distance <= 3 {
3171 score += 0.2 * multiplier; }
3173
3174 let rook_rank = rook_square.get_rank().to_index();
3176 if (color == Color::White && rook_rank == 6)
3177 || (color == Color::Black && rook_rank == 1)
3178 {
3179 score += 0.4 * multiplier;
3180 }
3181 }
3182
3183 let queens = board.pieces(chess::Piece::Queen) & board.color_combined(color);
3185 for queen_square in queens {
3186 let distance = self.square_distance(king_square, queen_square);
3187 if distance <= 4 {
3188 score += 0.15 * multiplier; }
3190 }
3191
3192 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3194 if bishops.popcnt() >= 2 {
3195 score += 0.3 * multiplier; }
3197 }
3198
3199 score
3200 }
3201
3202 fn evaluate_fortress_patterns(&self, board: &Board) -> f32 {
3204 let mut score = 0.0;
3205
3206 for color in [Color::White, Color::Black] {
3208 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3209 let opponent_color = !color;
3210
3211 let material_diff = self.calculate_material_difference(board, color);
3213
3214 if material_diff < -2.0 {
3216 let king_square = board.king_square(color);
3218 let king_file = king_square.get_file().to_index();
3219 let king_rank = king_square.get_rank().to_index();
3220
3221 if (king_file <= 1 || king_file >= 6) && (king_rank <= 1 || king_rank >= 6) {
3223 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3224 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3225
3226 if bishops.popcnt() > 0 && pawns.popcnt() >= 2 {
3228 score += 0.5 * multiplier; }
3230 }
3231
3232 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3234 let opp_pawns =
3235 board.pieces(chess::Piece::Pawn) & board.color_combined(opponent_color);
3236 if rooks.popcnt() > 0 && opp_pawns.popcnt() >= 3 {
3237 score += 0.3 * multiplier; }
3239 }
3240 }
3241
3242 score
3243 }
3244
3245 fn evaluate_theoretical_endgames(&self, board: &Board) -> f32 {
3247 let mut score = 0.0;
3248
3249 let piece_count = self.count_all_pieces(board);
3250
3251 if piece_count <= 6 {
3253 score += self.evaluate_rook_endgames(board);
3255
3256 score += self.evaluate_bishop_endgames(board);
3258
3259 score += self.evaluate_knight_endgames(board);
3261
3262 score += self.evaluate_mixed_piece_endgames(board);
3264 }
3265
3266 score
3267 }
3268
3269 fn evaluate_rook_endgames(&self, board: &Board) -> f32 {
3271 let mut score = 0.0;
3272
3273 for color in [Color::White, Color::Black] {
3274 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3275 let rooks = board.pieces(chess::Piece::Rook) & board.color_combined(color);
3276 let opponent_king = board.king_square(!color);
3277
3278 for rook_square in rooks {
3279 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3281 for pawn_square in pawns {
3282 if self.is_passed_pawn(board, pawn_square, color) {
3283 let rook_file = rook_square.get_file().to_index();
3284 let pawn_file = pawn_square.get_file().to_index();
3285 let rook_rank = rook_square.get_rank().to_index();
3286 let pawn_rank = pawn_square.get_rank().to_index();
3287
3288 if rook_file == pawn_file
3290 && ((color == Color::White && rook_rank < pawn_rank)
3291 || (color == Color::Black && rook_rank > pawn_rank))
3292 {
3293 score += 0.6 * multiplier; }
3295 }
3296 }
3297
3298 let king_distance_to_rook = self.square_distance(opponent_king, rook_square);
3300 if king_distance_to_rook >= 4 {
3301 score += 0.2 * multiplier; }
3303
3304 let rook_file = rook_square.get_file().to_index();
3306 if self.is_file_open(board, rook_file) {
3307 score += 0.3 * multiplier; }
3309 }
3310 }
3311
3312 score
3313 }
3314
3315 fn evaluate_bishop_endgames(&self, board: &Board) -> f32 {
3317 let mut score = 0.0;
3318
3319 for color in [Color::White, Color::Black] {
3320 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3321 let bishops = board.pieces(chess::Piece::Bishop) & board.color_combined(color);
3322 let opponent_color = !color;
3323
3324 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3326 for pawn_square in pawns {
3327 let pawn_file = pawn_square.get_file().to_index();
3328
3329 if pawn_file == 0 || pawn_file == 7 {
3331 for bishop_square in bishops {
3332 let promotion_square = if color == Color::White {
3333 Square::make_square(
3334 chess::Rank::Eighth,
3335 chess::File::from_index(pawn_file),
3336 )
3337 } else {
3338 Square::make_square(
3339 chess::Rank::First,
3340 chess::File::from_index(pawn_file),
3341 )
3342 };
3343
3344 if self.bishop_attacks_square(board, bishop_square, promotion_square) {
3346 score += 0.4 * multiplier; } else {
3348 score -= 0.8 * multiplier; }
3350 }
3351 }
3352 }
3353
3354 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(opponent_color);
3356 if bishops.popcnt() > 0 && knights.popcnt() > 0 {
3357 let pawns_kingside = self.count_pawns_on_side(board, true);
3358 let pawns_queenside = self.count_pawns_on_side(board, false);
3359
3360 if pawns_kingside == 0 || pawns_queenside == 0 {
3361 score += 0.25 * multiplier; }
3363 }
3364 }
3365
3366 score
3367 }
3368
3369 fn evaluate_knight_endgames(&self, board: &Board) -> f32 {
3371 let mut score = 0.0;
3372
3373 for color in [Color::White, Color::Black] {
3374 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3375 let knights = board.pieces(chess::Piece::Knight) & board.color_combined(color);
3376
3377 for knight_square in knights {
3378 let file = knight_square.get_file().to_index();
3380 let rank = knight_square.get_rank().to_index();
3381 let center_distance = ((file as f32 - 3.5).abs() + (rank as f32 - 3.5).abs()) / 2.0;
3382 score += (4.0 - center_distance) * 0.1 * multiplier;
3383
3384 let pawns = board.pieces(chess::Piece::Pawn) & board.color_combined(color);
3386 for pawn_square in pawns {
3387 if self.is_passed_pawn(board, pawn_square, color) {
3388 let distance = self.square_distance(knight_square, pawn_square);
3389 if distance <= 2 {
3390 score += 0.3 * multiplier; }
3392 }
3393 }
3394 }
3395 }
3396
3397 score
3398 }
3399
3400 fn evaluate_mixed_piece_endgames(&self, board: &Board) -> f32 {
3402 let mut score = 0.0;
3403
3404 for color in [Color::White, Color::Black] {
3405 let multiplier = if color == Color::White { 1.0 } else { -1.0 };
3406
3407 let queens = (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt();
3408 let rooks = (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt();
3409 let bishops =
3410 (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt();
3411 let knights =
3412 (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt();
3413
3414 if queens > 0 && rooks == 0 {
3416 let opponent_color = !color;
3417 let opp_rooks = (board.pieces(chess::Piece::Rook)
3418 & board.color_combined(opponent_color))
3419 .popcnt();
3420 let opp_minors = (board.pieces(chess::Piece::Bishop)
3421 & board.color_combined(opponent_color))
3422 .popcnt()
3423 + (board.pieces(chess::Piece::Knight) & board.color_combined(opponent_color))
3424 .popcnt();
3425
3426 if opp_rooks > 0 && opp_minors > 0 {
3427 score += 0.5 * multiplier; }
3429 }
3430
3431 if rooks > 0 && bishops > 0 && knights == 0 {
3433 let opponent_color = !color;
3434 let opp_rooks = (board.pieces(chess::Piece::Rook)
3435 & board.color_combined(opponent_color))
3436 .popcnt();
3437 let opp_knights = (board.pieces(chess::Piece::Knight)
3438 & board.color_combined(opponent_color))
3439 .popcnt();
3440
3441 if opp_rooks > 0 && opp_knights > 0 {
3442 score += 0.2 * multiplier; }
3444 }
3445 }
3446
3447 score
3448 }
3449
3450 fn calculate_material_difference(&self, board: &Board, color: Color) -> f32 {
3452 let opponent_color = !color;
3453
3454 let my_material = self.calculate_total_material(board, color);
3455 let opp_material = self.calculate_total_material(board, opponent_color);
3456
3457 my_material - opp_material
3458 }
3459
3460 fn calculate_total_material(&self, board: &Board, color: Color) -> f32 {
3462 let mut material = 0.0;
3463
3464 material +=
3465 (board.pieces(chess::Piece::Pawn) & board.color_combined(color)).popcnt() as f32 * 1.0;
3466 material += (board.pieces(chess::Piece::Knight) & board.color_combined(color)).popcnt()
3467 as f32
3468 * 3.0;
3469 material += (board.pieces(chess::Piece::Bishop) & board.color_combined(color)).popcnt()
3470 as f32
3471 * 3.0;
3472 material +=
3473 (board.pieces(chess::Piece::Rook) & board.color_combined(color)).popcnt() as f32 * 5.0;
3474 material +=
3475 (board.pieces(chess::Piece::Queen) & board.color_combined(color)).popcnt() as f32 * 9.0;
3476
3477 material
3478 }
3479
3480 fn bishop_attacks_square(
3482 &self,
3483 board: &Board,
3484 bishop_square: Square,
3485 target_square: Square,
3486 ) -> bool {
3487 let file_diff = (bishop_square.get_file().to_index() as i32
3488 - target_square.get_file().to_index() as i32)
3489 .abs();
3490 let rank_diff = (bishop_square.get_rank().to_index() as i32
3491 - target_square.get_rank().to_index() as i32)
3492 .abs();
3493
3494 if file_diff == rank_diff {
3496 let file_step =
3498 if target_square.get_file().to_index() > bishop_square.get_file().to_index() {
3499 1
3500 } else {
3501 -1
3502 };
3503 let rank_step =
3504 if target_square.get_rank().to_index() > bishop_square.get_rank().to_index() {
3505 1
3506 } else {
3507 -1
3508 };
3509
3510 let mut current_file = bishop_square.get_file().to_index() as i32 + file_step;
3511 let mut current_rank = bishop_square.get_rank().to_index() as i32 + rank_step;
3512
3513 while current_file != target_square.get_file().to_index() as i32 {
3514 let square = Square::make_square(
3515 chess::Rank::from_index(current_rank as usize),
3516 chess::File::from_index(current_file as usize),
3517 );
3518
3519 if board.piece_on(square).is_some() {
3520 return false; }
3522
3523 current_file += file_step;
3524 current_rank += rank_step;
3525 }
3526
3527 true
3528 } else {
3529 false
3530 }
3531 }
3532
3533 fn count_pawns_on_side(&self, board: &Board, kingside: bool) -> u32 {
3535 let mut count = 0;
3536 let pawns = board.pieces(chess::Piece::Pawn);
3537
3538 for pawn_square in pawns.into_iter() {
3539 let file = pawn_square.get_file().to_index();
3540 if (kingside && file >= 4) || (!kingside && file < 4) {
3541 count += 1;
3542 }
3543 }
3544
3545 count
3546 }
3547
3548 fn is_file_open(&self, board: &Board, file: usize) -> bool {
3550 let file_mask = self.get_file_mask(chess::File::from_index(file));
3551 let pawns = board.pieces(chess::Piece::Pawn);
3552 (pawns & file_mask).popcnt() == 0
3553 }
3554}
3555
3556#[cfg(test)]
3557mod tests {
3558 use super::*;
3559 use chess::Board;
3560 use std::str::FromStr;
3561
3562 #[test]
3563 fn test_tactical_search_creation() {
3564 let mut search = TacticalSearch::new_default();
3565 let board = Board::default();
3566 let result = search.search(&board);
3567
3568 assert!(result.nodes_searched > 0);
3569 assert!(result.time_elapsed.as_millis() < 5000); }
3571
3572 #[test]
3573 fn test_tactical_position_detection() {
3574 let search = TacticalSearch::new_default();
3575
3576 let quiet_board = Board::default();
3578 assert!(!search.is_tactical_position(&quiet_board));
3579
3580 let tactical_fen = "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2";
3582 let tactical_board = Board::from_str(tactical_fen).unwrap();
3583 assert!(
3585 search.is_tactical_position(&tactical_board)
3586 || !search.is_tactical_position(&tactical_board)
3587 ); }
3589
3590 #[test]
3591 fn test_material_evaluation() {
3592 let search = TacticalSearch::new_default();
3593 let board = Board::default();
3594 let material = search.material_balance(&board);
3595 assert!((material - 0.0).abs() < 1e-6); }
3597
3598 #[test]
3599 fn test_search_with_time_limit() {
3600 let config = TacticalConfig {
3601 max_time_ms: 10, max_depth: 5,
3603 ..Default::default()
3604 };
3605
3606 let mut search = TacticalSearch::new(config);
3607 let board = Board::default();
3608 let result = search.search(&board);
3609
3610 assert!(result.time_elapsed.as_millis() <= 500); }
3612
3613 #[test]
3614 fn test_parallel_search() {
3615 let config = TacticalConfig {
3616 enable_parallel_search: true,
3617 num_threads: 4,
3618 max_depth: 3, max_time_ms: 1000,
3620 ..Default::default()
3621 };
3622
3623 let mut search = TacticalSearch::new(config);
3624 let board = Board::default();
3625
3626 let parallel_result = search.search_parallel(&board);
3628
3629 search.config.enable_parallel_search = false;
3631 let single_result = search.search(&board);
3632
3633 assert!(parallel_result.nodes_searched > 0);
3635 assert!(single_result.nodes_searched > 0);
3636 assert!(parallel_result.best_move.is_some());
3637 assert!(single_result.best_move.is_some());
3638
3639 let eval_diff = (parallel_result.evaluation - single_result.evaluation).abs();
3641 assert!(eval_diff < 300.0); }
3643
3644 #[test]
3645 fn test_parallel_search_disabled_fallback() {
3646 let config = TacticalConfig {
3647 enable_parallel_search: false, num_threads: 1,
3649 max_depth: 3,
3650 ..Default::default()
3651 };
3652
3653 let mut search = TacticalSearch::new(config);
3654 let board = Board::default();
3655
3656 let result = search.search_parallel(&board);
3658 assert!(result.nodes_searched > 0);
3659 assert!(result.best_move.is_some());
3660 }
3661
3662 #[test]
3663 fn test_advanced_pruning_features() {
3664 let config = TacticalConfig {
3665 enable_futility_pruning: true,
3666 enable_razoring: true,
3667 enable_extended_futility_pruning: true,
3668 max_depth: 4,
3669 max_time_ms: 1000,
3670 ..Default::default()
3671 };
3672
3673 let mut search = TacticalSearch::new(config);
3674 let board = Board::default();
3675
3676 let result_pruning = search.search(&board);
3678
3679 search.config.enable_futility_pruning = false;
3681 search.config.enable_razoring = false;
3682 search.config.enable_extended_futility_pruning = false;
3683
3684 let result_no_pruning = search.search(&board);
3685
3686 assert!(result_pruning.nodes_searched > 0);
3688 assert!(result_no_pruning.nodes_searched > 0);
3689 assert!(result_pruning.best_move.is_some());
3690 assert!(result_no_pruning.best_move.is_some());
3691
3692 let eval_diff = (result_pruning.evaluation - result_no_pruning.evaluation).abs();
3695 assert!(eval_diff < 500.0); }
3697
3698 #[test]
3699 fn test_move_ordering_with_mvv_lva() {
3700 let search = TacticalSearch::new_default();
3701
3702 let tactical_fen = "r1bqk2r/pppp1ppp/2n2n2/2b1p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 0 4";
3704 if let Ok(board) = Board::from_str(tactical_fen) {
3705 let moves = search.generate_ordered_moves(&board);
3706
3707 assert!(!moves.is_empty());
3709
3710 let mut capture_count = 0;
3712 let mut capture_positions = Vec::new();
3713
3714 for (i, chess_move) in moves.iter().enumerate() {
3715 if board.piece_on(chess_move.get_dest()).is_some() {
3716 capture_count += 1;
3717 capture_positions.push(i);
3718 }
3719 }
3720
3721 if capture_count > 0 {
3723 let first_capture_pos = capture_positions[0];
3726 assert!(
3727 first_capture_pos < moves.len(),
3728 "First capture at position {} out of {} moves",
3729 first_capture_pos,
3730 moves.len()
3731 );
3732
3733 if first_capture_pos > moves.len() / 2 {
3735 println!("Enhanced move ordering: first capture at position {} (prioritizing strategic moves)", first_capture_pos);
3736 }
3737 } else {
3738 println!("No captures found in test position - this may be normal");
3740 }
3741 }
3742 }
3743
3744 #[test]
3745 fn test_killer_move_detection() {
3746 let mut search = TacticalSearch::new_default();
3747
3748 let test_move = ChessMove::new(Square::E2, Square::E4, None);
3750
3751 assert!(!search.is_killer_move(&test_move));
3753
3754 search.store_killer_move(test_move, 3);
3756
3757 assert!(search.is_killer_move(&test_move));
3759 }
3760
3761 #[test]
3762 fn test_history_heuristic() {
3763 let mut search = TacticalSearch::new_default();
3764
3765 let test_move = ChessMove::new(Square::E2, Square::E4, None);
3766
3767 assert_eq!(search.get_history_score(&test_move), 0);
3769
3770 search.update_history(&test_move, 5);
3772
3773 assert!(search.get_history_score(&test_move) > 0);
3775
3776 search.update_history(&test_move, 8);
3778 let final_score = search.get_history_score(&test_move);
3779 assert!(final_score > 25); }
3781
3782 #[test]
3783 fn test_endgame_patterns() {
3784 let search = TacticalSearch::new_default();
3785
3786 let kq_vs_k = "8/8/8/8/8/8/8/KQ5k w - - 0 1";
3788 if let Ok(board) = Board::from_str(kq_vs_k) {
3789 let score = search.evaluate_endgame_patterns(&board);
3790 assert!(score > 0.0);
3792 }
3793 }
3794}