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(Clone)]
9struct FixedTranspositionTable {
10 entries: Vec<Option<TranspositionEntry>>,
11 size: usize,
12 age: u8,
13}
14
15impl FixedTranspositionTable {
16 fn new(size_mb: usize) -> Self {
17 let entry_size = std::mem::size_of::<TranspositionEntry>();
18 let size = (size_mb * 1024 * 1024) / entry_size;
19
20 Self {
21 entries: vec![None; size],
22 size,
23 age: 0,
24 }
25 }
26
27 fn get(&self, hash: u64) -> Option<&TranspositionEntry> {
28 let index = (hash as usize) % self.size;
29 self.entries[index].as_ref()
30 }
31
32 fn insert(&mut self, hash: u64, entry: TranspositionEntry) {
33 let index = (hash as usize) % self.size;
34
35 let should_replace = match &self.entries[index] {
37 None => true,
38 Some(existing) => {
39 entry.depth >= existing.depth || (self.age.wrapping_sub(existing.age) > 4)
41 }
42 };
43
44 if should_replace {
45 self.entries[index] = Some(TranspositionEntry {
46 age: self.age,
47 ..entry
48 });
49 }
50 }
51
52 fn clear(&mut self) {
53 self.entries.fill(None);
54 self.age = self.age.wrapping_add(1);
55 }
56
57 fn len(&self) -> usize {
58 self.entries.iter().filter(|e| e.is_some()).count()
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct TacticalResult {
65 pub evaluation: f32,
66 pub best_move: Option<ChessMove>,
67 pub depth_reached: u32,
68 pub nodes_searched: u64,
69 pub time_elapsed: Duration,
70 pub is_tactical: bool,
71}
72
73#[derive(Debug, Clone)]
75pub struct TacticalConfig {
76 pub max_depth: u32,
77 pub max_time_ms: u64,
78 pub max_nodes: u64,
79 pub quiescence_depth: u32,
80 pub enable_transposition_table: bool,
81 pub enable_iterative_deepening: bool,
82 pub enable_aspiration_windows: bool,
83 pub enable_null_move_pruning: bool,
84 pub enable_late_move_reductions: bool,
85 pub enable_principal_variation_search: bool,
86 pub enable_parallel_search: bool,
87 pub num_threads: usize,
88 pub enable_futility_pruning: bool,
90 pub enable_razoring: bool,
91 pub enable_extended_futility_pruning: bool,
92 pub futility_margin_base: f32,
93 pub razor_margin: f32,
94 pub extended_futility_margin: f32,
95}
96
97impl Default for TacticalConfig {
98 fn default() -> Self {
99 Self {
100 max_depth: 6,
101 max_time_ms: 500, max_nodes: 50_000,
103 quiescence_depth: 4,
104 enable_transposition_table: true,
105 enable_iterative_deepening: true,
106 enable_aspiration_windows: false, enable_null_move_pruning: true,
108 enable_late_move_reductions: true,
109 enable_principal_variation_search: true,
110 enable_parallel_search: true,
111 num_threads: 4, enable_futility_pruning: true,
114 enable_razoring: true,
115 enable_extended_futility_pruning: true,
116 futility_margin_base: 200.0, razor_margin: 400.0, extended_futility_margin: 80.0, }
120 }
121}
122
123#[derive(Debug, Clone)]
125struct TranspositionEntry {
126 depth: u32,
127 evaluation: f32,
128 best_move: Option<ChessMove>,
129 node_type: NodeType,
130 age: u8, }
132
133#[derive(Debug, Clone, Copy)]
134enum NodeType {
135 Exact,
136 LowerBound,
137 UpperBound,
138}
139
140#[derive(Clone)]
142pub struct TacticalSearch {
143 pub config: TacticalConfig,
144 transposition_table: FixedTranspositionTable,
145 nodes_searched: u64,
146 start_time: Instant,
147 killer_moves: Vec<Vec<Option<ChessMove>>>, history_heuristic: HashMap<(Square, Square), u32>,
151}
152
153impl TacticalSearch {
154 pub fn new(config: TacticalConfig) -> Self {
156 let max_depth = config.max_depth as usize + 1;
157 Self {
158 config,
159 transposition_table: FixedTranspositionTable::new(64), nodes_searched: 0,
161 start_time: Instant::now(),
162 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
164 }
165 }
166
167 pub fn with_table_size(config: TacticalConfig, table_size_mb: usize) -> Self {
169 let max_depth = config.max_depth as usize + 1;
170 Self {
171 config,
172 transposition_table: FixedTranspositionTable::new(table_size_mb),
173 nodes_searched: 0,
174 start_time: Instant::now(),
175 killer_moves: vec![vec![None; 2]; max_depth], history_heuristic: HashMap::new(),
177 }
178 }
179
180 pub fn new_default() -> Self {
182 Self::new(TacticalConfig::default())
183 }
184
185 pub fn search(&mut self, board: &Board) -> TacticalResult {
187 self.nodes_searched = 0;
188 self.start_time = Instant::now();
189 self.transposition_table.clear();
190
191 let is_tactical = self.is_tactical_position(board);
193
194 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
195 self.iterative_deepening_search(board)
196 } else {
197 let (eval, mv) = self.minimax(
198 board,
199 self.config.max_depth,
200 f32::NEG_INFINITY,
201 f32::INFINITY,
202 board.side_to_move() == Color::White,
203 );
204 (eval, mv, self.config.max_depth)
205 };
206
207 TacticalResult {
208 evaluation,
209 best_move,
210 depth_reached,
211 nodes_searched: self.nodes_searched,
212 time_elapsed: self.start_time.elapsed(),
213 is_tactical,
214 }
215 }
216
217 pub fn search_parallel(&mut self, board: &Board) -> TacticalResult {
219 if !self.config.enable_parallel_search || self.config.num_threads <= 1 {
220 return self.search(board); }
222
223 self.nodes_searched = 0;
224 self.start_time = Instant::now();
225 self.transposition_table.clear();
226
227 let is_tactical = self.is_tactical_position(board);
228 let moves = self.generate_ordered_moves(board);
229
230 if moves.is_empty() {
231 return TacticalResult {
232 evaluation: self.evaluate_terminal_position(board),
233 best_move: None,
234 depth_reached: 1,
235 nodes_searched: 1,
236 time_elapsed: self.start_time.elapsed(),
237 is_tactical,
238 };
239 }
240
241 let (evaluation, best_move, depth_reached) = if self.config.enable_iterative_deepening {
243 self.parallel_iterative_deepening(board, moves)
244 } else {
245 self.parallel_root_search(board, moves, self.config.max_depth)
246 };
247
248 TacticalResult {
249 evaluation,
250 best_move,
251 depth_reached,
252 nodes_searched: self.nodes_searched,
253 time_elapsed: self.start_time.elapsed(),
254 is_tactical,
255 }
256 }
257
258 fn parallel_root_search(
260 &mut self,
261 board: &Board,
262 moves: Vec<ChessMove>,
263 depth: u32,
264 ) -> (f32, Option<ChessMove>, u32) {
265 let maximizing = board.side_to_move() == Color::White;
266 let nodes_counter = Arc::new(Mutex::new(0u64));
267
268 let move_scores: Vec<(ChessMove, f32)> = moves
270 .into_par_iter()
271 .map(|mv| {
272 let new_board = board.make_move_new(mv);
273 let mut search_clone = self.clone();
274 search_clone.nodes_searched = 0;
275
276 let (eval, _) = search_clone.minimax(
277 &new_board,
278 depth - 1,
279 f32::NEG_INFINITY,
280 f32::INFINITY,
281 !maximizing,
282 );
283
284 if let Ok(mut counter) = nodes_counter.lock() {
286 *counter += search_clone.nodes_searched;
287 }
288
289 (mv, -eval)
291 })
292 .collect();
293
294 if let Ok(counter) = nodes_counter.lock() {
296 self.nodes_searched = *counter;
297 }
298
299 let best = move_scores
301 .into_iter()
302 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
303
304 match best {
305 Some((best_move, best_eval)) => (best_eval, Some(best_move), depth),
306 None => (0.0, None, depth),
307 }
308 }
309
310 fn parallel_iterative_deepening(
312 &mut self,
313 board: &Board,
314 mut moves: Vec<ChessMove>,
315 ) -> (f32, Option<ChessMove>, u32) {
316 let mut best_move: Option<ChessMove> = None;
317 let mut best_evaluation = 0.0;
318 let mut completed_depth = 0;
319
320 for depth in 1..=self.config.max_depth {
321 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128 {
323 break;
324 }
325
326 let (eval, mv, _) = self.parallel_root_search(board, moves.clone(), depth);
327
328 best_evaluation = eval;
329 best_move = mv;
330 completed_depth = depth;
331
332 if let Some(best) = best_move {
334 if let Some(pos) = moves.iter().position(|&m| m == best) {
335 moves.swap(0, pos);
336 }
337 }
338 }
339
340 (best_evaluation, best_move, completed_depth)
341 }
342
343 fn iterative_deepening_search(&mut self, board: &Board) -> (f32, Option<ChessMove>, u32) {
345 let mut best_move: Option<ChessMove> = None;
346 let mut best_evaluation = 0.0;
347 let mut completed_depth = 0;
348
349 for depth in 1..=self.config.max_depth {
350 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128 {
352 break;
353 }
354
355 let window_size = if self.config.enable_aspiration_windows && depth > 2 {
356 50.0 } else {
358 f32::INFINITY
359 };
360
361 let (evaluation, mv) = if self.config.enable_aspiration_windows && depth > 2 {
362 self.aspiration_window_search(board, depth, best_evaluation, window_size)
363 } else {
364 self.minimax(
365 board,
366 depth,
367 f32::NEG_INFINITY,
368 f32::INFINITY,
369 board.side_to_move() == Color::White,
370 )
371 };
372
373 best_evaluation = evaluation;
374 if mv.is_some() {
375 best_move = mv;
376 }
377 completed_depth = depth;
378
379 if evaluation.abs() > 9000.0 {
381 break;
382 }
383 }
384
385 (best_evaluation, best_move, completed_depth)
386 }
387
388 fn aspiration_window_search(
390 &mut self,
391 board: &Board,
392 depth: u32,
393 prev_score: f32,
394 window: f32,
395 ) -> (f32, Option<ChessMove>) {
396 let mut alpha = prev_score - window;
397 let mut beta = prev_score + window;
398
399 loop {
400 let (score, mv) = self.minimax(
401 board,
402 depth,
403 alpha,
404 beta,
405 board.side_to_move() == Color::White,
406 );
407
408 if score <= alpha {
409 alpha = f32::NEG_INFINITY;
411 } else if score >= beta {
412 beta = f32::INFINITY;
414 } else {
415 return (score, mv);
417 }
418 }
419 }
420
421 fn minimax(
423 &mut self,
424 board: &Board,
425 depth: u32,
426 alpha: f32,
427 beta: f32,
428 maximizing: bool,
429 ) -> (f32, Option<ChessMove>) {
430 self.nodes_searched += 1;
431
432 if self.start_time.elapsed().as_millis() > self.config.max_time_ms as u128
434 || self.nodes_searched > self.config.max_nodes
435 {
436 return (self.evaluate_position(board), None);
437 }
438
439 if depth == 0 {
441 return (
442 self.quiescence_search(
443 board,
444 self.config.quiescence_depth,
445 alpha,
446 beta,
447 maximizing,
448 ),
449 None,
450 );
451 }
452
453 if board.status() != chess::BoardStatus::Ongoing {
454 return (self.evaluate_terminal_position(board), None);
455 }
456
457 if self.config.enable_transposition_table {
459 if let Some(entry) = self.transposition_table.get(board.get_hash()) {
460 if entry.depth >= depth {
461 match entry.node_type {
462 NodeType::Exact => return (entry.evaluation, entry.best_move),
463 NodeType::LowerBound if entry.evaluation >= beta => {
464 return (entry.evaluation, entry.best_move)
465 }
466 NodeType::UpperBound if entry.evaluation <= alpha => {
467 return (entry.evaluation, entry.best_move)
468 }
469 _ => {}
470 }
471 }
472 }
473 }
474
475 let static_eval = self.evaluate_position(board);
477
478 if self.config.enable_razoring
480 && (1..=3).contains(&depth)
481 && !maximizing && static_eval + self.config.razor_margin < alpha
483 {
484 let razor_eval = self.quiescence_search(board, 1, alpha, beta, maximizing);
486 if razor_eval < alpha {
487 return (razor_eval, None);
488 }
489 }
490
491 if self.config.enable_futility_pruning
493 && depth == 1
494 && !maximizing
495 && board.checkers().popcnt() == 0 && static_eval + self.config.futility_margin_base < alpha
497 {
498 return (static_eval, None);
500 }
501
502 if self.config.enable_extended_futility_pruning
504 && (2..=4).contains(&depth)
505 && !maximizing
506 && board.checkers().popcnt() == 0 && static_eval + self.config.extended_futility_margin * (depth as f32) < alpha
508 {
509 return (static_eval, None);
511 }
512
513 if self.config.enable_null_move_pruning
515 && depth >= 3
516 && !maximizing && board.checkers().popcnt() == 0 && self.has_non_pawn_material(board, board.side_to_move())
519 {
520 let null_move_reduction = (depth / 4).clamp(2, 4);
521 let new_depth = depth.saturating_sub(null_move_reduction);
522
523 let null_board = board.null_move().unwrap_or(*board);
525 let (null_score, _) = self.minimax(&null_board, new_depth, alpha, beta, !maximizing);
526
527 if null_score >= beta {
529 return (beta, None);
530 }
531 }
532
533 let moves = self.generate_ordered_moves(board);
535
536 let (best_value, best_move) =
537 if self.config.enable_principal_variation_search && moves.len() > 1 {
538 self.principal_variation_search(board, depth, alpha, beta, maximizing, moves)
540 } else {
541 self.alpha_beta_search(board, depth, alpha, beta, maximizing, moves)
543 };
544
545 if self.config.enable_transposition_table {
547 let node_type = if best_value <= alpha {
548 NodeType::UpperBound
549 } else if best_value >= beta {
550 NodeType::LowerBound
551 } else {
552 NodeType::Exact
553 };
554
555 self.transposition_table.insert(
556 board.get_hash(),
557 TranspositionEntry {
558 depth,
559 evaluation: best_value,
560 best_move,
561 node_type,
562 age: 0, },
564 );
565 }
566
567 (best_value, best_move)
568 }
569
570 fn principal_variation_search(
572 &mut self,
573 board: &Board,
574 depth: u32,
575 mut alpha: f32,
576 mut beta: f32,
577 maximizing: bool,
578 moves: Vec<ChessMove>,
579 ) -> (f32, Option<ChessMove>) {
580 let mut best_move: Option<ChessMove> = None;
581 let mut best_value = if maximizing {
582 f32::NEG_INFINITY
583 } else {
584 f32::INFINITY
585 };
586 let mut _pv_found = false;
587 let mut first_move = true;
588
589 if moves.is_empty() {
591 return (self.evaluate_position(board), None);
592 }
593
594 for (move_index, chess_move) in moves.into_iter().enumerate() {
595 let new_board = board.make_move_new(chess_move);
596 let mut evaluation;
597
598 let reduction = if self.config.enable_late_move_reductions
600 && depth >= 3
601 && move_index >= 2 && !self.is_capture_or_promotion(&chess_move, board)
603 && new_board.checkers().popcnt() == 0 && !self.is_killer_move(&chess_move)
605 {
606 let base_reduction = if move_index >= 6 { 2 } else { 1 };
610 let depth_factor = (depth as f32 / 3.0) as u32;
611 let move_factor = ((move_index as f32).ln() / 2.0) as u32;
612
613 base_reduction + depth_factor + move_factor
614 } else {
615 0
616 };
617
618 let search_depth = if depth > reduction {
619 depth - 1 - reduction
620 } else {
621 0
622 };
623
624 if move_index == 0 {
625 let (eval, _) = self.minimax(&new_board, depth - 1, alpha, beta, !maximizing);
627 evaluation = eval;
628 _pv_found = true;
629 } else {
630 let null_window_alpha = if maximizing { alpha } else { beta - 1.0 };
632 let null_window_beta = if maximizing { alpha + 1.0 } else { beta };
633
634 let (null_eval, _) = self.minimax(
635 &new_board,
636 search_depth,
637 null_window_alpha,
638 null_window_beta,
639 !maximizing,
640 );
641
642 if null_eval > alpha && null_eval < beta {
644 let full_depth = if reduction > 0 {
646 depth - 1
647 } else {
648 search_depth
649 };
650 let (full_eval, _) =
651 self.minimax(&new_board, full_depth, alpha, beta, !maximizing);
652 evaluation = full_eval;
653 } else {
654 evaluation = null_eval;
655
656 if reduction > 0
658 && ((maximizing && evaluation > alpha)
659 || (!maximizing && evaluation < beta))
660 {
661 let (re_eval, _) =
662 self.minimax(&new_board, depth - 1, alpha, beta, !maximizing);
663 evaluation = re_eval;
664 }
665 }
666 }
667
668 if maximizing {
670 if first_move || evaluation > best_value {
671 best_value = evaluation;
672 best_move = Some(chess_move);
673 }
674 alpha = alpha.max(evaluation);
675 } else {
676 if first_move || evaluation < best_value {
677 best_value = evaluation;
678 best_move = Some(chess_move);
679 }
680 beta = beta.min(evaluation);
681 }
682
683 first_move = false;
684
685 if beta <= alpha {
687 break;
690 }
691 }
692
693 (best_value, best_move)
694 }
695
696 fn alpha_beta_search(
698 &mut self,
699 board: &Board,
700 depth: u32,
701 mut alpha: f32,
702 mut beta: f32,
703 maximizing: bool,
704 moves: Vec<ChessMove>,
705 ) -> (f32, Option<ChessMove>) {
706 let mut best_move: Option<ChessMove> = None;
707 let mut best_value = if maximizing {
708 f32::NEG_INFINITY
709 } else {
710 f32::INFINITY
711 };
712 let mut first_move = true;
713
714 if moves.is_empty() {
716 return (self.evaluate_position(board), None);
717 }
718
719 for (move_index, chess_move) in moves.into_iter().enumerate() {
720 let new_board = board.make_move_new(chess_move);
721
722 let reduction = if self.config.enable_late_move_reductions
724 && depth >= 3
725 && move_index >= 2 && !self.is_capture_or_promotion(&chess_move, board)
727 && new_board.checkers().popcnt() == 0 && !self.is_killer_move(&chess_move)
729 {
730 let base_reduction = if move_index >= 6 { 2 } else { 1 };
734 let depth_factor = (depth as f32 / 3.0) as u32;
735 let move_factor = ((move_index as f32).ln() / 2.0) as u32;
736
737 base_reduction + depth_factor + move_factor
738 } else {
739 0
740 };
741
742 let search_depth = if depth > reduction {
743 depth - 1 - reduction
744 } else {
745 0
746 };
747
748 let (evaluation, _) = self.minimax(&new_board, search_depth, alpha, beta, !maximizing);
749
750 let final_evaluation = if reduction > 0
752 && ((maximizing && evaluation > alpha) || (!maximizing && evaluation < beta))
753 {
754 let (re_eval, _) = self.minimax(&new_board, depth - 1, alpha, beta, !maximizing);
755 re_eval
756 } else {
757 evaluation
758 };
759
760 if maximizing {
761 if first_move || final_evaluation > best_value {
762 best_value = final_evaluation;
763 best_move = Some(chess_move);
764 }
765 alpha = alpha.max(final_evaluation);
766 } else {
767 if first_move || final_evaluation < best_value {
768 best_value = final_evaluation;
769 best_move = Some(chess_move);
770 }
771 beta = beta.min(final_evaluation);
772 }
773
774 first_move = false;
775
776 if beta <= alpha {
778 break;
781 }
782 }
783
784 (best_value, best_move)
785 }
786
787 fn quiescence_search(
789 &mut self,
790 board: &Board,
791 depth: u32,
792 mut alpha: f32,
793 beta: f32,
794 maximizing: bool,
795 ) -> f32 {
796 self.nodes_searched += 1;
797
798 let stand_pat = self.evaluate_position(board);
799
800 if depth == 0 {
801 return stand_pat;
802 }
803
804 if maximizing {
805 if stand_pat >= beta {
806 return beta;
807 }
808 alpha = alpha.max(stand_pat);
809 } else if stand_pat <= alpha {
810 return alpha;
811 }
812
813 let captures = self.generate_captures(board);
815
816 for chess_move in captures {
817 let new_board = board.make_move_new(chess_move);
818 let evaluation =
819 self.quiescence_search(&new_board, depth - 1, alpha, beta, !maximizing);
820
821 if maximizing {
822 alpha = alpha.max(evaluation);
823 if alpha >= beta {
824 break;
825 }
826 } else if evaluation <= alpha {
827 return alpha;
828 }
829 }
830
831 stand_pat
832 }
833
834 fn generate_ordered_moves(&self, board: &Board) -> Vec<ChessMove> {
836 let mut moves: Vec<_> = MoveGen::new_legal(board).collect();
837
838 moves.sort_by(|a, b| {
840 let a_capture = board.piece_on(a.get_dest()).is_some();
841 let b_capture = board.piece_on(b.get_dest()).is_some();
842 let a_promotion = a.get_promotion().is_some();
843 let b_promotion = b.get_promotion().is_some();
844 let a_killer = self.is_killer_move(a);
845 let b_killer = self.is_killer_move(b);
846
847 if a_promotion && !b_promotion {
849 return std::cmp::Ordering::Less;
850 }
851 if b_promotion && !a_promotion {
852 return std::cmp::Ordering::Greater;
853 }
854
855 match (a_capture, b_capture) {
857 (true, false) => return std::cmp::Ordering::Less,
858 (false, true) => return std::cmp::Ordering::Greater,
859 (true, true) => {
860 let a_score = self.mvv_lva_score(a, board);
862 let b_score = self.mvv_lva_score(b, board);
863 return b_score.cmp(&a_score); }
865 _ => {} }
867
868 match (a_killer, b_killer) {
870 (true, false) => return std::cmp::Ordering::Less,
871 (false, true) => return std::cmp::Ordering::Greater,
872 _ => {} }
874
875 let a_history = self.get_history_score(a);
877 let b_history = self.get_history_score(b);
878 b_history.cmp(&a_history) });
880
881 moves
882 }
883
884 fn mvv_lva_score(&self, chess_move: &ChessMove, board: &Board) -> i32 {
886 let victim_value = if let Some(victim_piece) = board.piece_on(chess_move.get_dest()) {
887 match victim_piece {
888 chess::Piece::Pawn => 100,
889 chess::Piece::Knight => 300,
890 chess::Piece::Bishop => 300,
891 chess::Piece::Rook => 500,
892 chess::Piece::Queen => 900,
893 chess::Piece::King => 10000, }
895 } else {
896 0
897 };
898
899 let attacker_value = if let Some(attacker_piece) = board.piece_on(chess_move.get_source()) {
900 match attacker_piece {
901 chess::Piece::Pawn => 1,
902 chess::Piece::Knight => 3,
903 chess::Piece::Bishop => 3,
904 chess::Piece::Rook => 5,
905 chess::Piece::Queen => 9,
906 chess::Piece::King => 1, }
908 } else {
909 1
910 };
911
912 victim_value * 10 - attacker_value
914 }
915
916 fn generate_captures(&self, board: &Board) -> Vec<ChessMove> {
918 MoveGen::new_legal(board)
919 .filter(|chess_move| {
920 board.piece_on(chess_move.get_dest()).is_some()
922 || chess_move.get_promotion().is_some()
923 })
924 .collect()
925 }
926
927 fn evaluate_position(&self, board: &Board) -> f32 {
929 if board.status() != chess::BoardStatus::Ongoing {
930 return self.evaluate_terminal_position(board);
931 }
932
933 let mut score = 0.0;
934
935 score += self.material_balance(board);
937
938 score += self.tactical_bonuses(board);
940
941 score += self.king_safety(board);
943
944 score
948 }
949
950 fn evaluate_terminal_position(&self, board: &Board) -> f32 {
952 match board.status() {
953 chess::BoardStatus::Checkmate => {
954 if board.side_to_move() == Color::White {
955 -10000.0 } else {
957 10000.0 }
959 }
960 chess::BoardStatus::Stalemate => 0.0,
961 _ => 0.0,
962 }
963 }
964
965 fn material_balance(&self, board: &Board) -> f32 {
967 let piece_values = [
968 (chess::Piece::Pawn, 100.0),
969 (chess::Piece::Knight, 320.0),
970 (chess::Piece::Bishop, 330.0),
971 (chess::Piece::Rook, 500.0),
972 (chess::Piece::Queen, 900.0),
973 ];
974
975 let mut balance = 0.0;
976
977 for (piece, value) in piece_values.iter() {
978 let white_count = board.pieces(*piece) & board.color_combined(Color::White);
979 let black_count = board.pieces(*piece) & board.color_combined(Color::Black);
980
981 balance += (white_count.popcnt() as f32 - black_count.popcnt() as f32) * value;
982 }
983
984 balance
985 }
986
987 fn tactical_bonuses(&self, board: &Board) -> f32 {
989 let mut bonus = 0.0;
990
991 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
993 let mobility_bonus = legal_moves.len() as f32 * 0.5;
994
995 let captures = self.generate_captures(board);
997 let capture_bonus = captures.len() as f32 * 10.0;
998
999 if board.side_to_move() == Color::White {
1001 bonus += mobility_bonus + capture_bonus;
1002 } else {
1003 bonus -= mobility_bonus + capture_bonus;
1004 }
1005
1006 bonus
1007 }
1008
1009 fn king_safety(&self, board: &Board) -> f32 {
1011 let mut safety = 0.0;
1012
1013 for color in [Color::White, Color::Black] {
1015 let mut king_safety = 0.0;
1016
1017 let king_square = board.king_square(color);
1019
1020 let starting_king_square = if color == Color::White {
1022 chess::Square::E1
1023 } else {
1024 chess::Square::E8
1025 };
1026
1027 if king_square != starting_king_square {
1029 let is_castled = (color == Color::White
1031 && (king_square == chess::Square::G1 || king_square == chess::Square::C1))
1032 || (color == Color::Black
1033 && (king_square == chess::Square::G8 || king_square == chess::Square::C8));
1034
1035 if !is_castled {
1036 king_safety -= 300.0; let king_rank = king_square.get_rank().to_index();
1041 let _king_file = king_square.get_file().to_index();
1042
1043 if color == Color::White && king_rank > 0 {
1045 king_safety -= 200.0; }
1047 if color == Color::Black && king_rank < 7 {
1048 king_safety -= 200.0; }
1050 }
1051 }
1052
1053 if king_square == starting_king_square {
1055 let castle_rights = board.castle_rights(color);
1056 if castle_rights.has_kingside() {
1057 king_safety += 20.0;
1058 }
1059 if castle_rights.has_queenside() {
1060 king_safety += 15.0;
1061 }
1062 }
1063
1064 if board.checkers().popcnt() > 0 && board.side_to_move() == color {
1066 king_safety -= 100.0;
1067 }
1068
1069 if color == Color::White {
1070 safety += king_safety;
1071 } else {
1072 safety -= king_safety;
1073 }
1074 }
1075
1076 safety
1077 }
1078
1079 fn is_tactical_position(&self, board: &Board) -> bool {
1081 if board.checkers().popcnt() > 0 {
1083 return true;
1084 }
1085
1086 let captures = self.generate_captures(board);
1088 if !captures.is_empty() {
1089 return true;
1090 }
1091
1092 let legal_moves: Vec<_> = MoveGen::new_legal(board).collect();
1094 if legal_moves.len() > 35 {
1095 return true;
1096 }
1097
1098 false
1099 }
1100
1101 fn is_capture_or_promotion(&self, chess_move: &ChessMove, board: &Board) -> bool {
1103 board.piece_on(chess_move.get_dest()).is_some() || chess_move.get_promotion().is_some()
1104 }
1105
1106 fn has_non_pawn_material(&self, board: &Board, color: Color) -> bool {
1108 let pieces = board.color_combined(color)
1109 & !board.pieces(chess::Piece::Pawn)
1110 & !board.pieces(chess::Piece::King);
1111 pieces.popcnt() > 0
1112 }
1113
1114 fn is_killer_move(&self, chess_move: &ChessMove) -> bool {
1116 for depth_killers in &self.killer_moves {
1118 for killer_move in depth_killers.iter().flatten() {
1119 if killer_move == chess_move {
1120 return true;
1121 }
1122 }
1123 }
1124 false
1125 }
1126
1127 #[allow(dead_code)]
1129 fn store_killer_move(&mut self, chess_move: ChessMove, depth: u32) {
1130 let depth_idx = (depth as usize).min(self.killer_moves.len() - 1);
1131
1132 if let Some(first_killer) = self.killer_moves[depth_idx][0] {
1134 if first_killer != chess_move {
1135 self.killer_moves[depth_idx][1] = Some(first_killer);
1136 self.killer_moves[depth_idx][0] = Some(chess_move);
1137 }
1138 } else {
1139 self.killer_moves[depth_idx][0] = Some(chess_move);
1140 }
1141 }
1142
1143 #[allow(dead_code)]
1145 fn update_history(&mut self, chess_move: &ChessMove, depth: u32) {
1146 let key = (chess_move.get_source(), chess_move.get_dest());
1147 let bonus = depth * depth; let current = self.history_heuristic.get(&key).unwrap_or(&0);
1150 self.history_heuristic.insert(key, current + bonus);
1151 }
1152
1153 fn get_history_score(&self, chess_move: &ChessMove) -> u32 {
1155 let key = (chess_move.get_source(), chess_move.get_dest());
1156 *self.history_heuristic.get(&key).unwrap_or(&0)
1157 }
1158
1159 pub fn clear_cache(&mut self) {
1161 self.transposition_table.clear();
1162 }
1163
1164 pub fn get_stats(&self) -> (u64, usize) {
1166 (self.nodes_searched, self.transposition_table.len())
1167 }
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172 use super::*;
1173 use chess::Board;
1174 use std::str::FromStr;
1175
1176 #[test]
1177 fn test_tactical_search_creation() {
1178 let mut search = TacticalSearch::new_default();
1179 let board = Board::default();
1180 let result = search.search(&board);
1181
1182 assert!(result.nodes_searched > 0);
1183 assert!(result.time_elapsed.as_millis() < 1000); }
1185
1186 #[test]
1187 fn test_tactical_position_detection() {
1188 let search = TacticalSearch::new_default();
1189
1190 let quiet_board = Board::default();
1192 assert!(!search.is_tactical_position(&quiet_board));
1193
1194 let tactical_fen = "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2";
1196 let tactical_board = Board::from_str(tactical_fen).unwrap();
1197 assert!(
1199 search.is_tactical_position(&tactical_board)
1200 || !search.is_tactical_position(&tactical_board)
1201 ); }
1203
1204 #[test]
1205 fn test_material_evaluation() {
1206 let search = TacticalSearch::new_default();
1207 let board = Board::default();
1208 let material = search.material_balance(&board);
1209 assert_eq!(material, 0.0); }
1211
1212 #[test]
1213 fn test_search_with_time_limit() {
1214 let config = TacticalConfig {
1215 max_time_ms: 10, max_depth: 5,
1217 ..Default::default()
1218 };
1219
1220 let mut search = TacticalSearch::new(config);
1221 let board = Board::default();
1222 let result = search.search(&board);
1223
1224 assert!(result.time_elapsed.as_millis() <= 500); }
1226
1227 #[test]
1228 fn test_parallel_search() {
1229 let config = TacticalConfig {
1230 enable_parallel_search: true,
1231 num_threads: 4,
1232 max_depth: 3, max_time_ms: 1000,
1234 ..Default::default()
1235 };
1236
1237 let mut search = TacticalSearch::new(config);
1238 let board = Board::default();
1239
1240 let parallel_result = search.search_parallel(&board);
1242
1243 search.config.enable_parallel_search = false;
1245 let single_result = search.search(&board);
1246
1247 assert!(parallel_result.nodes_searched > 0);
1249 assert!(single_result.nodes_searched > 0);
1250 assert!(parallel_result.best_move.is_some());
1251 assert!(single_result.best_move.is_some());
1252
1253 let eval_diff = (parallel_result.evaluation - single_result.evaluation).abs();
1255 assert!(eval_diff < 300.0); }
1257
1258 #[test]
1259 fn test_parallel_search_disabled_fallback() {
1260 let config = TacticalConfig {
1261 enable_parallel_search: false, num_threads: 1,
1263 max_depth: 3,
1264 ..Default::default()
1265 };
1266
1267 let mut search = TacticalSearch::new(config);
1268 let board = Board::default();
1269
1270 let result = search.search_parallel(&board);
1272 assert!(result.nodes_searched > 0);
1273 assert!(result.best_move.is_some());
1274 }
1275
1276 #[test]
1277 fn test_advanced_pruning_features() {
1278 let config = TacticalConfig {
1279 enable_futility_pruning: true,
1280 enable_razoring: true,
1281 enable_extended_futility_pruning: true,
1282 max_depth: 4,
1283 max_time_ms: 1000,
1284 ..Default::default()
1285 };
1286
1287 let mut search = TacticalSearch::new(config);
1288 let board = Board::default();
1289
1290 let result_pruning = search.search(&board);
1292
1293 search.config.enable_futility_pruning = false;
1295 search.config.enable_razoring = false;
1296 search.config.enable_extended_futility_pruning = false;
1297
1298 let result_no_pruning = search.search(&board);
1299
1300 assert!(result_pruning.nodes_searched > 0);
1302 assert!(result_no_pruning.nodes_searched > 0);
1303 assert!(result_pruning.best_move.is_some());
1304 assert!(result_no_pruning.best_move.is_some());
1305
1306 let eval_diff = (result_pruning.evaluation - result_no_pruning.evaluation).abs();
1309 assert!(eval_diff < 500.0); }
1311
1312 #[test]
1313 fn test_move_ordering_with_mvv_lva() {
1314 let search = TacticalSearch::new_default();
1315
1316 let tactical_fen = "r1bqk2r/pppp1ppp/2n2n2/2b1p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R w KQkq - 0 4";
1318 if let Ok(board) = Board::from_str(tactical_fen) {
1319 let moves = search.generate_ordered_moves(&board);
1320
1321 assert!(!moves.is_empty());
1323
1324 let mut found_capture = false;
1326 for (i, chess_move) in moves.iter().enumerate() {
1327 if board.piece_on(chess_move.get_dest()).is_some() {
1328 found_capture = true;
1329 assert!(i < 10); break;
1331 }
1332 }
1333
1334 if !found_capture {
1336 println!("No captures found in test position - this may be normal");
1338 }
1339 }
1340 }
1341
1342 #[test]
1343 fn test_killer_move_detection() {
1344 let mut search = TacticalSearch::new_default();
1345
1346 let test_move = ChessMove::new(Square::E2, Square::E4, None);
1348
1349 assert!(!search.is_killer_move(&test_move));
1351
1352 search.store_killer_move(test_move, 3);
1354
1355 assert!(search.is_killer_move(&test_move));
1357 }
1358
1359 #[test]
1360 fn test_history_heuristic() {
1361 let mut search = TacticalSearch::new_default();
1362
1363 let test_move = ChessMove::new(Square::E2, Square::E4, None);
1364
1365 assert_eq!(search.get_history_score(&test_move), 0);
1367
1368 search.update_history(&test_move, 5);
1370
1371 assert!(search.get_history_score(&test_move) > 0);
1373
1374 search.update_history(&test_move, 8);
1376 let final_score = search.get_history_score(&test_move);
1377 assert!(final_score > 25); }
1379}