1use chess::{Board, ChessMove, Color};
2use std::collections::HashMap;
3use std::io::{self, BufRead, Write};
4use std::str::FromStr;
5use std::thread;
6use std::time::{Duration, Instant};
7
8use crate::{ChessVectorEngine, HybridConfig, TacticalConfig};
9
10pub struct UCIEngine {
12 engine: ChessVectorEngine,
13 board: Board,
14 debug: bool,
15 engine_name: String,
16 engine_author: String,
17 options: HashMap<String, UCIOption>,
18 thinking: bool,
19 stop_search: bool,
20 pondering: bool,
21 ponder_move: Option<ChessMove>,
22 ponder_board: Option<Board>,
23}
24
25#[derive(Debug, Clone)]
27pub enum UCIOption {
28 Check {
29 default: bool,
30 value: bool,
31 },
32 Spin {
33 default: i32,
34 min: i32,
35 max: i32,
36 value: i32,
37 },
38 Combo {
39 default: String,
40 options: Vec<String>,
41 value: String,
42 },
43 Button,
44 String {
45 default: String,
46 value: String,
47 },
48}
49
50#[derive(Debug, Clone)]
52pub struct SearchInfo {
53 depth: u32,
54 #[allow(dead_code)]
55 seldepth: Option<u32>,
56 time: u64, nodes: u64,
58 nps: u64, #[allow(dead_code)]
60 score: SearchScore,
61 #[allow(dead_code)]
62 pv: Vec<ChessMove>, #[allow(dead_code)]
64 currmove: Option<ChessMove>,
65 #[allow(dead_code)]
66 currmovenumber: Option<u32>,
67}
68
69#[derive(Debug, Clone)]
70pub enum SearchScore {
71 Centipawns(i32),
72 Mate(i32), }
74
75impl UCIEngine {
76 pub fn new() -> Self {
77 let mut engine = ChessVectorEngine::new_lightweight(1024);
78
79 engine.enable_opening_book();
81 engine.enable_tactical_search_default();
82 engine.configure_hybrid_evaluation(HybridConfig::default());
83
84 let _ = engine.enable_strategic_motifs();
86
87 let mut options = HashMap::new();
90
91 options.insert(
93 "Hash".to_string(),
94 UCIOption::Spin {
95 default: 128,
96 min: 1,
97 max: 2048,
98 value: 128,
99 },
100 );
101
102 options.insert(
103 "Threads".to_string(),
104 UCIOption::Spin {
105 default: 1,
106 min: 1,
107 max: 64,
108 value: 1,
109 },
110 );
111
112 options.insert(
113 "MultiPV".to_string(),
114 UCIOption::Spin {
115 default: 1,
116 min: 1,
117 max: 10,
118 value: 1,
119 },
120 );
121
122 options.insert(
124 "Pattern_Weight".to_string(),
125 UCIOption::Spin {
126 default: 60,
127 min: 0,
128 max: 100,
129 value: 60,
130 },
131 );
132
133 options.insert(
134 "Tactical_Depth".to_string(),
135 UCIOption::Spin {
136 default: 3,
137 min: 1,
138 max: 10,
139 value: 3,
140 },
141 );
142
143 options.insert(
144 "Pattern_Confidence_Threshold".to_string(),
145 UCIOption::Spin {
146 default: 75,
147 min: 0,
148 max: 100,
149 value: 75,
150 },
151 );
152
153 options.insert(
154 "Enable_LSH".to_string(),
155 UCIOption::Check {
156 default: true,
157 value: true,
158 },
159 );
160
161 options.insert(
162 "Enable_GPU".to_string(),
163 UCIOption::Check {
164 default: true,
165 value: true,
166 },
167 );
168
169 options.insert(
170 "Ponder".to_string(),
171 UCIOption::Check {
172 default: true,
173 value: true,
174 },
175 );
176
177 options.insert("Load_Position_Data".to_string(), UCIOption::Button);
178
179 Self {
180 engine,
181 board: Board::default(),
182 debug: false,
183 engine_name: "Chess Vector Engine".to_string(),
184 engine_author: "Chess Vector Engine Team".to_string(),
185 options,
186 thinking: false,
187 stop_search: false,
188 pondering: false,
189 ponder_move: None,
190 ponder_board: None,
191 }
192 }
193
194 pub fn run(&mut self) {
196 let stdin = io::stdin();
197 let mut stdout = io::stdout();
198
199 for line in stdin.lock().lines() {
200 match line {
201 Ok(command) => {
202 let response = self.process_command(command.trim());
203 if !response.is_empty() {
204 let _ = writeln!(stdout, "{response}");
205 let _ = stdout.flush();
206 }
207
208 if command.trim() == "quit" {
209 break;
210 }
211 }
212 Err(e) => {
213 if self.debug {
214 let _ = writeln!(stdout, "Error reading input: {e}");
215 }
216 break;
217 }
218 }
219 }
220 }
221
222 fn process_command(&mut self, command: &str) -> String {
224 let parts: Vec<&str> = command.split_whitespace().collect();
225 if parts.is_empty() {
226 return String::new();
227 }
228
229 match parts[0] {
230 "uci" => self.handle_uci(),
231 "debug" => self.handle_debug(&parts),
232 "isready" => self.handle_isready(),
233 "setoption" => self.handle_setoption(&parts),
234 "register" => String::new(), "ucinewgame" => self.handle_ucinewgame(),
236 "position" => self.handle_position(&parts),
237 "go" => self.handle_go(&parts),
238 "stop" => self.handle_stop(),
239 "ponderhit" => self.handle_ponderhit(),
240 "quit" => String::new(),
241 _ => {
242 if self.debug {
243 format!("Unknown command: {command}")
244 } else {
245 String::new()
246 }
247 }
248 }
249 }
250
251 fn handle_uci(&self) -> String {
252 let mut response = String::new();
253 response.push_str(&format!("id name {}\n", self.engine_name));
254 response.push_str(&format!("id author {}\n", self.engine_author));
255
256 for (name, option) in &self.options {
258 match option {
259 UCIOption::Check { default, .. } => {
260 response.push_str(&format!(
261 "option name {name} type check default {default}\n"
262 ));
263 }
264 UCIOption::Spin {
265 default, min, max, ..
266 } => {
267 response.push_str(&format!(
268 "option name {name} type spin default {default} min {min} max {max}\n"
269 ));
270 }
271 UCIOption::Combo {
272 default, options, ..
273 } => {
274 let combo_options = options.join(" var ");
275 response.push_str(&format!(
276 "option name {name} type combo default {default} var {combo_options}\n"
277 ));
278 }
279 UCIOption::Button => {
280 response.push_str(&format!("option name {name} type button\n"));
281 }
282 UCIOption::String { default, .. } => {
283 response.push_str(&format!(
284 "option name {name} type string default {default}\n"
285 ));
286 }
287 }
288 }
289
290 response.push_str("uciok");
291 response
292 }
293
294 fn handle_debug(&mut self, parts: &[&str]) -> String {
295 if parts.len() >= 2 {
296 match parts[1] {
297 "on" => self.debug = true,
298 "off" => self.debug = false,
299 _ => {}
300 }
301 }
302 String::new()
303 }
304
305 fn handle_isready(&self) -> String {
306 "readyok".to_string()
307 }
308
309 fn handle_setoption(&mut self, parts: &[&str]) -> String {
310 if parts.len() >= 4 && parts[1] == "name" {
312 let mut name_parts = Vec::new();
313 let mut value_parts = Vec::new();
314 let mut in_value = false;
315
316 for &part in &parts[2..] {
317 if part == "value" {
318 in_value = true;
319 } else if in_value {
320 value_parts.push(part);
321 } else {
322 name_parts.push(part);
323 }
324 }
325
326 let name = name_parts.join(" ");
327 let value = value_parts.join(" ");
328
329 self.set_option(&name, &value);
330 }
331
332 String::new()
333 }
334
335 fn set_option(&mut self, name: &str, value: &str) {
336 if let Some(option) = self.options.get_mut(name) {
337 match option {
338 UCIOption::Check {
339 value: ref mut val, ..
340 } => {
341 *val = value == "true";
342 }
343 UCIOption::Spin {
344 value: ref mut val,
345 min,
346 max,
347 ..
348 } => {
349 if let Ok(new_val) = value.parse::<i32>() {
350 if new_val >= *min && new_val <= *max {
351 *val = new_val;
352 }
353 }
354 }
355 UCIOption::Combo {
356 value: ref mut val,
357 options,
358 ..
359 } => {
360 if options.contains(&value.to_string()) {
361 *val = value.to_string();
362 }
363 }
364 UCIOption::String {
365 value: ref mut val, ..
366 } => {
367 *val = value.to_string();
368 }
369 UCIOption::Button => {
370 if name == "Load_Position_Data" {
372 let _ = self.engine.auto_load_training_data();
374 }
375 }
376 }
377 }
378
379 self.apply_options();
381 }
382
383 fn apply_options(&mut self) {
384 if let Some(UCIOption::Spin {
385 value: pattern_weight,
386 ..
387 }) = self.options.get("Pattern_Weight")
388 {
389 let weight = (*pattern_weight as f32) / 100.0;
390 let config = HybridConfig {
391 pattern_weight: weight,
392 ..HybridConfig::default()
393 };
394 self.engine.configure_hybrid_evaluation(config);
395 }
396
397 if let Some(UCIOption::Spin { value: depth, .. }) = self.options.get("Tactical_Depth") {
398 let config = TacticalConfig {
399 max_depth: *depth as u32,
400 ..TacticalConfig::default()
401 };
402 self.engine.enable_tactical_search(config);
403 }
404
405 if let Some(UCIOption::Spin {
406 value: threshold, ..
407 }) = self.options.get("Pattern_Confidence_Threshold")
408 {
409 let config = HybridConfig {
410 pattern_confidence_threshold: (*threshold as f32) / 100.0,
411 ..HybridConfig::default()
412 };
413 self.engine.configure_hybrid_evaluation(config);
414 }
415
416 if let Some(UCIOption::Check {
417 value: enable_lsh, ..
418 }) = self.options.get("Enable_LSH")
419 {
420 if *enable_lsh && !self.engine.is_lsh_enabled() {
421 self.engine.enable_lsh(8, 16);
422 }
423 }
424 }
425
426 fn handle_ucinewgame(&mut self) -> String {
427 self.board = Board::default();
428 String::new()
430 }
431
432 fn handle_position(&mut self, parts: &[&str]) -> String {
433 if parts.len() < 2 {
434 return String::new();
435 }
436
437 let mut board = if parts[1] == "startpos" {
438 Board::default()
439 } else if parts[1] == "fen" && parts.len() >= 8 {
440 let fen = parts[2..8].join(" ");
441 match Board::from_str(&fen) {
442 Ok(board) => board,
443 Err(_) => {
444 if self.debug {
445 return "info string Invalid FEN".to_string();
446 }
447 return String::new();
448 }
449 }
450 } else {
451 return String::new();
452 };
453
454 if let Some(moves_idx) = parts.iter().position(|&x| x == "moves") {
456 for move_str in &parts[moves_idx + 1..] {
457 if let Ok(chess_move) = ChessMove::from_str(move_str) {
458 if board.legal(chess_move) {
459 board = board.make_move_new(chess_move);
460 } else if self.debug {
461 return format!("info string Illegal move: {move_str}");
462 }
463 }
464 }
465 }
466
467 self.board = board;
468 String::new()
469 }
470
471 fn handle_go(&mut self, parts: &[&str]) -> String {
472 if self.thinking {
473 return String::new();
474 }
475
476 let mut wtime = None;
478 let mut btime = None;
479 let mut _winc: Option<u64> = None;
480 let mut _binc: Option<u64> = None;
481 let mut movestogo = None;
482 let mut depth = None;
483 let mut _nodes: Option<u64> = None;
484 let mut movetime = None;
485 let mut infinite = false;
486 let mut ponder = false;
487
488 let mut i = 1;
489 while i < parts.len() {
490 match parts[i] {
491 "wtime" if i + 1 < parts.len() => {
492 wtime = parts[i + 1].parse().ok();
493 i += 2;
494 }
495 "btime" if i + 1 < parts.len() => {
496 btime = parts[i + 1].parse().ok();
497 i += 2;
498 }
499 "winc" if i + 1 < parts.len() => {
500 _winc = parts[i + 1].parse().ok();
501 i += 2;
502 }
503 "binc" if i + 1 < parts.len() => {
504 _binc = parts[i + 1].parse().ok();
505 i += 2;
506 }
507 "movestogo" if i + 1 < parts.len() => {
508 movestogo = parts[i + 1].parse().ok();
509 i += 2;
510 }
511 "depth" if i + 1 < parts.len() => {
512 depth = parts[i + 1].parse().ok();
513 i += 2;
514 }
515 "nodes" if i + 1 < parts.len() => {
516 _nodes = parts[i + 1].parse().ok();
517 i += 2;
518 }
519 "movetime" if i + 1 < parts.len() => {
520 movetime = parts[i + 1].parse().ok();
521 i += 2;
522 }
523 "infinite" => {
524 infinite = true;
525 i += 1;
526 }
527 "ponder" => {
528 ponder = true;
529 i += 1;
530 }
531 _ => i += 1,
532 }
533 }
534
535 let search_time = if let Some(mt) = movetime {
537 Duration::from_millis(mt)
538 } else if infinite {
539 Duration::from_secs(3600) } else {
541 let our_time = if self.board.side_to_move() == Color::White {
543 wtime.unwrap_or(30000)
544 } else {
545 btime.unwrap_or(30000)
546 };
547
548 let moves_left = movestogo.unwrap_or(30);
549 let time_per_move = our_time / moves_left.max(1);
550 Duration::from_millis(time_per_move.min(our_time / 2))
551 };
552
553 if ponder {
555 self.start_ponder_search(search_time, depth);
556 } else {
557 self.start_search(search_time, depth);
558 }
559
560 String::new()
561 }
562
563 fn start_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
564 self.thinking = true;
565 self.stop_search = false;
566
567 let board = self.board;
568 let mut engine = self.engine.clone(); let start_time = Instant::now();
570
571 let multi_pv = if let Some(UCIOption::Spin { value, .. }) = self.options.get("MultiPV") {
573 *value as usize
574 } else {
575 1
576 };
577
578 thread::spawn(move || {
580 let mut search_info = SearchInfo {
581 depth: 1,
582 seldepth: None,
583 time: 0,
584 nodes: 0,
585 nps: 0,
586 score: SearchScore::Centipawns(0),
587 pv: Vec::new(),
588 currmove: None,
589 currmovenumber: None,
590 };
591
592 let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&board).collect();
594
595 if legal_moves.is_empty() {
596 println!("bestmove 0000"); return;
598 }
599
600 let mut move_evaluations: Vec<(ChessMove, f32)> = Vec::new();
601 let mut nodes_searched = 0;
602
603 use crate::evaluation_calibration::CalibratedEvaluator;
605 let calibrated_evaluator = CalibratedEvaluator::new(
606 crate::evaluation_calibration::CalibrationConfig::default()
607 );
608
609 for chess_move in &legal_moves {
611 let temp_board = board.make_move_new(*chess_move);
612 nodes_searched += 1;
613
614 let position_eval = if let Some(entry) = engine.get_opening_entry(&temp_board) {
616 entry.evaluation
617 } else {
618 calibrated_evaluator.evaluate_centipawns(&temp_board) as f32 / 100.0
620 };
621 {
622 let eval_for_us = if board.side_to_move() == chess::Color::White {
624 position_eval
625 } else {
626 -position_eval
627 };
628
629 move_evaluations.push((*chess_move, eval_for_us));
630 }
631 }
632
633 move_evaluations
635 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
636
637 let (best_move, _) = if !move_evaluations.is_empty() {
639 move_evaluations[0]
640 } else {
641 (legal_moves[0], 0.0)
643 };
644
645 search_info.time = start_time.elapsed().as_millis() as u64;
647 search_info.nodes = nodes_searched;
648 search_info.nps = if search_info.time > 0 {
649 (search_info.nodes * 1000) / search_info.time
650 } else {
651 0
652 };
653
654 let pv_count = move_evaluations.len().min(multi_pv);
656 for (pv_index, (chess_move, eval)) in move_evaluations.iter().take(pv_count).enumerate()
657 {
658 let score_cp = (*eval * 100.0) as i32;
659
660 if multi_pv == 1 {
662 println!(
664 "info depth {} score cp {} time {} nodes {} nps {} pv {}",
665 search_info.depth,
666 score_cp,
667 search_info.time,
668 search_info.nodes,
669 search_info.nps,
670 chess_move
671 );
672 } else {
673 println!(
675 "info depth {} multipv {} score cp {} time {} nodes {} nps {} pv {}",
676 search_info.depth,
677 pv_index + 1,
678 score_cp,
679 search_info.time,
680 search_info.nodes,
681 search_info.nps,
682 chess_move
683 );
684 }
685 }
686
687 println!("bestmove {best_move}");
689 });
690 }
691
692 fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
694 if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
696 if !value {
697 return; }
699 } else {
700 return; }
702
703 self.pondering = true;
704 self.thinking = true;
705 self.stop_search = false;
706
707 if let Some(ponder_move) = self.get_expected_opponent_move() {
709 self.ponder_move = Some(ponder_move);
710 self.ponder_board = Some(self.board.make_move_new(ponder_move));
711
712 if self.debug {
713 println!("info string Pondering on {ponder_move}");
714 }
715
716 let ponder_board = self.ponder_board.unwrap();
718 let mut engine = self.engine.clone();
719 let debug = self.debug;
720
721 thread::spawn(move || {
722 if let Some(eval) = engine.evaluate_position(&ponder_board) {
724 if debug {
725 println!("info string Ponder evaluation: {eval:.2}");
726 }
727 }
728
729 });
735 }
736 }
737
738 fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
740 use chess::MoveGen;
741
742 let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
748
749 if !legal_moves.is_empty() {
750 let mut best_move = legal_moves[0];
752 let mut best_eval = f32::NEG_INFINITY;
753
754 for chess_move in legal_moves.iter().take(5) {
755 let new_board = self.board.make_move_new(*chess_move);
757 if let Some(eval) = self.engine.evaluate_position(&new_board) {
758 let opponent_eval = -eval;
760 if opponent_eval > best_eval {
761 best_eval = opponent_eval;
762 best_move = *chess_move;
763 }
764 }
765 }
766
767 Some(best_move)
768 } else {
769 None
770 }
771 }
772
773 fn handle_ponderhit(&mut self) -> String {
775 if self.pondering {
776 self.pondering = false;
777
778 if self.debug {
779 println!("info string Ponderhit received - converting ponder to normal search");
780 }
781
782 if let Some(ponder_board) = self.ponder_board {
790 let legal_moves: Vec<ChessMove> =
791 chess::MoveGen::new_legal(&ponder_board).collect();
792 if !legal_moves.is_empty() {
793 let mut best_move = legal_moves[0];
795 let mut best_eval = f32::NEG_INFINITY;
796
797 for chess_move in legal_moves.iter().take(5) {
798 let new_board = ponder_board.make_move_new(*chess_move);
799 if let Some(eval) = self.engine.evaluate_position(&new_board) {
800 if eval > best_eval {
801 best_eval = eval;
802 best_move = *chess_move;
803 }
804 }
805 }
806 thread::spawn(move || {
807 println!("bestmove {best_move}");
808 });
809 }
810 }
811
812 self.ponder_move = None;
813 self.ponder_board = None;
814 }
815
816 String::new()
817 }
818
819 fn handle_stop(&mut self) -> String {
820 self.stop_search = true;
821 self.thinking = false;
822
823 if self.pondering {
825 self.pondering = false;
826 self.ponder_move = None;
827 self.ponder_board = None;
828
829 if self.debug {
830 println!("info string Pondering stopped");
831 }
832 }
833
834 String::new()
835 }
836}
837
838fn calculate_material_eval(board: &Board) -> f32 {
840 let mut white_material = 0.0;
841 let mut black_material = 0.0;
842
843 const PAWN_VALUE: f32 = 1.0;
845 const KNIGHT_VALUE: f32 = 3.0;
846 const BISHOP_VALUE: f32 = 3.0;
847 const ROOK_VALUE: f32 = 5.0;
848 const QUEEN_VALUE: f32 = 9.0;
849
850 for square in chess::ALL_SQUARES {
851 if let Some(piece) = board.piece_on(square) {
852 let value = match piece {
853 chess::Piece::Pawn => PAWN_VALUE,
854 chess::Piece::Knight => KNIGHT_VALUE,
855 chess::Piece::Bishop => BISHOP_VALUE,
856 chess::Piece::Rook => ROOK_VALUE,
857 chess::Piece::Queen => QUEEN_VALUE,
858 chess::Piece::King => 0.0, };
860
861 if board.color_on(square) == Some(chess::Color::White) {
862 white_material += value;
863 } else {
864 black_material += value;
865 }
866 }
867 }
868
869 white_material - black_material
871}
872
873impl Default for UCIEngine {
874 fn default() -> Self {
875 Self::new()
876 }
877}
878
879#[derive(Debug, Clone)]
881pub struct UCIConfig {
882 pub engine_name: String,
883 pub engine_author: String,
884 pub enable_debug: bool,
885 pub default_hash_size: i32,
886 pub default_threads: i32,
887}
888
889impl Default for UCIConfig {
890 fn default() -> Self {
891 Self {
892 engine_name: "Chess Vector Engine".to_string(),
893 engine_author: "Chess Vector Engine Team".to_string(),
894 enable_debug: false,
895 default_hash_size: 128,
896 default_threads: 1,
897 }
898 }
899}
900
901pub fn run_uci_engine() {
903 let mut engine = UCIEngine::new();
904 engine.run();
905}
906
907pub fn run_uci_engine_with_config(config: UCIConfig) {
909 let mut engine = UCIEngine::new();
910 engine.engine_name = config.engine_name;
911 engine.engine_author = config.engine_author;
912 engine.debug = config.enable_debug;
913
914 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Hash") {
916 *value = config.default_hash_size;
917 }
918 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Threads") {
919 *value = config.default_threads;
920 }
921
922 engine.run();
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928 use std::str::FromStr;
929
930 #[test]
931 fn test_uci_initialization() {
932 let engine = UCIEngine::new();
933 assert_eq!(engine.board, Board::default());
934 assert!(!engine.debug);
935 assert!(!engine.thinking);
936 }
937
938 #[test]
939 fn test_uci_command() {
940 let engine = UCIEngine::new();
941 let response = engine.handle_uci();
942 assert!(response.contains("id name"));
943 assert!(response.contains("id author"));
944 assert!(response.contains("uciok"));
945 }
946
947 #[test]
948 fn test_isready_command() {
949 let engine = UCIEngine::new();
950 let response = engine.handle_isready();
951 assert_eq!(response, "readyok");
952 }
953
954 #[test]
955 fn test_position_startpos() {
956 let mut engine = UCIEngine::new();
957 let parts = vec!["position", "startpos"];
958 engine.handle_position(&parts);
959 assert_eq!(engine.board, Board::default());
960 }
961
962 #[test]
963 fn test_position_with_moves() {
964 let mut engine = UCIEngine::new();
965 let parts = vec!["position", "startpos", "moves", "e2e4", "e7e5"];
966 engine.handle_position(&parts);
967
968 let expected_board = Board::default()
969 .make_move_new(ChessMove::from_str("e2e4").unwrap())
970 .make_move_new(ChessMove::from_str("e7e5").unwrap());
971
972 assert_eq!(engine.board, expected_board);
973 }
974
975 #[test]
976 fn test_option_setting() {
977 let mut engine = UCIEngine::new();
978 engine.set_option("Pattern_Weight", "80");
979
980 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("Pattern_Weight") {
981 assert_eq!(*value, 80);
982 } else {
983 panic!("Option not found or wrong type");
984 }
985 }
986
987 #[test]
988 fn test_debug_toggle() {
989 let mut engine = UCIEngine::new();
990
991 engine.handle_debug(&["debug", "on"]);
992 assert!(engine.debug);
993
994 engine.handle_debug(&["debug", "off"]);
995 assert!(!engine.debug);
996 }
997
998 #[test]
999 fn test_pondering_option() {
1000 let engine = UCIEngine::new();
1001
1002 if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
1004 assert!(*value); } else {
1006 panic!("Ponder option should be available");
1007 }
1008 }
1009
1010 #[test]
1011 fn test_ponder_command_parsing() {
1012 let mut engine = UCIEngine::new();
1013
1014 let response = engine.process_command("go ponder");
1016 assert_eq!(response, ""); }
1021
1022 #[test]
1023 fn test_ponderhit_command() {
1024 let mut engine = UCIEngine::new();
1025
1026 engine.pondering = true;
1028 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
1029
1030 let response = engine.handle_ponderhit();
1032 assert_eq!(response, "");
1033
1034 assert!(!engine.pondering);
1036 assert!(engine.ponder_move.is_none());
1037 }
1038
1039 #[test]
1040 fn test_stop_during_pondering() {
1041 let mut engine = UCIEngine::new();
1042
1043 engine.pondering = true;
1045 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
1046
1047 let response = engine.handle_stop();
1049 assert_eq!(response, "");
1050
1051 assert!(!engine.pondering);
1053 assert!(!engine.thinking);
1054 assert!(engine.ponder_move.is_none());
1055 }
1056
1057 #[test]
1058 fn test_expected_opponent_move() {
1059 let mut engine = UCIEngine::new();
1060
1061 let expected_move = engine.get_expected_opponent_move();
1063 assert!(expected_move.is_some());
1064
1065 let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&engine.board).collect();
1067 if let Some(mv) = expected_move {
1068 assert!(legal_moves.contains(&mv));
1069 }
1070 }
1071
1072 #[test]
1073 fn test_multi_pv_option() {
1074 let engine = UCIEngine::new();
1075
1076 if let Some(UCIOption::Spin {
1078 default,
1079 min,
1080 max,
1081 value,
1082 }) = engine.options.get("MultiPV")
1083 {
1084 assert_eq!(*default, 1);
1085 assert_eq!(*min, 1);
1086 assert_eq!(*max, 10);
1087 assert_eq!(*value, 1);
1088 } else {
1089 panic!("MultiPV option should be available");
1090 }
1091 }
1092
1093 #[test]
1094 fn test_multi_pv_setting() {
1095 let mut engine = UCIEngine::new();
1096
1097 let response = engine.process_command("setoption name MultiPV value 3");
1099 assert_eq!(response, "");
1100
1101 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1103 assert_eq!(*value, 3);
1104 } else {
1105 panic!("MultiPV option should exist");
1106 }
1107 }
1108
1109 #[test]
1110 fn test_single_pv_vs_multi_pv() {
1111 let mut engine = UCIEngine::new();
1112
1113 engine.process_command("setoption name MultiPV value 1");
1118 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1119 assert_eq!(*value, 1);
1120 }
1121
1122 engine.process_command("setoption name MultiPV value 5");
1124 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1125 assert_eq!(*value, 5);
1126 }
1127 }
1128}