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 for chess_move in &legal_moves {
605 let temp_board = board.make_move_new(*chess_move);
606 nodes_searched += 1;
607
608 if let Some(position_eval) = engine.evaluate_position(&temp_board) {
610 let eval_for_us = if board.side_to_move() == chess::Color::White {
612 position_eval
613 } else {
614 -position_eval
615 };
616
617 move_evaluations.push((*chess_move, eval_for_us));
618 }
619 }
620
621 move_evaluations
623 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
624
625 let (best_move, _) = if !move_evaluations.is_empty() {
627 move_evaluations[0]
628 } else {
629 (legal_moves[0], 0.0)
631 };
632
633 search_info.time = start_time.elapsed().as_millis() as u64;
635 search_info.nodes = nodes_searched;
636 search_info.nps = if search_info.time > 0 {
637 (search_info.nodes * 1000) / search_info.time
638 } else {
639 0
640 };
641
642 let pv_count = move_evaluations.len().min(multi_pv);
644 for (pv_index, (chess_move, eval)) in move_evaluations.iter().take(pv_count).enumerate()
645 {
646 let score_cp = (*eval * 100.0) as i32;
647
648 if multi_pv == 1 {
650 println!(
652 "info depth {} score cp {} time {} nodes {} nps {} pv {}",
653 search_info.depth,
654 score_cp,
655 search_info.time,
656 search_info.nodes,
657 search_info.nps,
658 chess_move
659 );
660 } else {
661 println!(
663 "info depth {} multipv {} score cp {} time {} nodes {} nps {} pv {}",
664 search_info.depth,
665 pv_index + 1,
666 score_cp,
667 search_info.time,
668 search_info.nodes,
669 search_info.nps,
670 chess_move
671 );
672 }
673 }
674
675 println!("bestmove {best_move}");
677 });
678 }
679
680 fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
682 if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
684 if !value {
685 return; }
687 } else {
688 return; }
690
691 self.pondering = true;
692 self.thinking = true;
693 self.stop_search = false;
694
695 if let Some(ponder_move) = self.get_expected_opponent_move() {
697 self.ponder_move = Some(ponder_move);
698 self.ponder_board = Some(self.board.make_move_new(ponder_move));
699
700 if self.debug {
701 println!("info string Pondering on {ponder_move}");
702 }
703
704 let ponder_board = self.ponder_board.unwrap();
706 let mut engine = self.engine.clone();
707 let debug = self.debug;
708
709 thread::spawn(move || {
710 if let Some(eval) = engine.evaluate_position(&ponder_board) {
712 if debug {
713 println!("info string Ponder evaluation: {eval:.2}");
714 }
715 }
716
717 });
723 }
724 }
725
726 fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
728 use chess::MoveGen;
729
730 let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
736
737 if !legal_moves.is_empty() {
738 let mut best_move = legal_moves[0];
740 let mut best_eval = f32::NEG_INFINITY;
741
742 for chess_move in legal_moves.iter().take(5) {
743 let new_board = self.board.make_move_new(*chess_move);
745 if let Some(eval) = self.engine.evaluate_position(&new_board) {
746 let opponent_eval = -eval;
748 if opponent_eval > best_eval {
749 best_eval = opponent_eval;
750 best_move = *chess_move;
751 }
752 }
753 }
754
755 Some(best_move)
756 } else {
757 None
758 }
759 }
760
761 fn handle_ponderhit(&mut self) -> String {
763 if self.pondering {
764 self.pondering = false;
765
766 if self.debug {
767 println!("info string Ponderhit received - converting ponder to normal search");
768 }
769
770 if let Some(ponder_board) = self.ponder_board {
778 let legal_moves: Vec<ChessMove> =
779 chess::MoveGen::new_legal(&ponder_board).collect();
780 if !legal_moves.is_empty() {
781 let mut best_move = legal_moves[0];
783 let mut best_eval = f32::NEG_INFINITY;
784
785 for chess_move in legal_moves.iter().take(5) {
786 let new_board = ponder_board.make_move_new(*chess_move);
787 if let Some(eval) = self.engine.evaluate_position(&new_board) {
788 if eval > best_eval {
789 best_eval = eval;
790 best_move = *chess_move;
791 }
792 }
793 }
794 thread::spawn(move || {
795 println!("bestmove {best_move}");
796 });
797 }
798 }
799
800 self.ponder_move = None;
801 self.ponder_board = None;
802 }
803
804 String::new()
805 }
806
807 fn handle_stop(&mut self) -> String {
808 self.stop_search = true;
809 self.thinking = false;
810
811 if self.pondering {
813 self.pondering = false;
814 self.ponder_move = None;
815 self.ponder_board = None;
816
817 if self.debug {
818 println!("info string Pondering stopped");
819 }
820 }
821
822 String::new()
823 }
824}
825
826impl Default for UCIEngine {
827 fn default() -> Self {
828 Self::new()
829 }
830}
831
832#[derive(Debug, Clone)]
834pub struct UCIConfig {
835 pub engine_name: String,
836 pub engine_author: String,
837 pub enable_debug: bool,
838 pub default_hash_size: i32,
839 pub default_threads: i32,
840}
841
842impl Default for UCIConfig {
843 fn default() -> Self {
844 Self {
845 engine_name: "Chess Vector Engine".to_string(),
846 engine_author: "Chess Vector Engine Team".to_string(),
847 enable_debug: false,
848 default_hash_size: 128,
849 default_threads: 1,
850 }
851 }
852}
853
854pub fn run_uci_engine() {
856 let mut engine = UCIEngine::new();
857 engine.run();
858}
859
860pub fn run_uci_engine_with_config(config: UCIConfig) {
862 let mut engine = UCIEngine::new();
863 engine.engine_name = config.engine_name;
864 engine.engine_author = config.engine_author;
865 engine.debug = config.enable_debug;
866
867 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Hash") {
869 *value = config.default_hash_size;
870 }
871 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Threads") {
872 *value = config.default_threads;
873 }
874
875 engine.run();
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881 use std::str::FromStr;
882
883 #[test]
884 fn test_uci_initialization() {
885 let engine = UCIEngine::new();
886 assert_eq!(engine.board, Board::default());
887 assert!(!engine.debug);
888 assert!(!engine.thinking);
889 }
890
891 #[test]
892 fn test_uci_command() {
893 let engine = UCIEngine::new();
894 let response = engine.handle_uci();
895 assert!(response.contains("id name"));
896 assert!(response.contains("id author"));
897 assert!(response.contains("uciok"));
898 }
899
900 #[test]
901 fn test_isready_command() {
902 let engine = UCIEngine::new();
903 let response = engine.handle_isready();
904 assert_eq!(response, "readyok");
905 }
906
907 #[test]
908 fn test_position_startpos() {
909 let mut engine = UCIEngine::new();
910 let parts = vec!["position", "startpos"];
911 engine.handle_position(&parts);
912 assert_eq!(engine.board, Board::default());
913 }
914
915 #[test]
916 fn test_position_with_moves() {
917 let mut engine = UCIEngine::new();
918 let parts = vec!["position", "startpos", "moves", "e2e4", "e7e5"];
919 engine.handle_position(&parts);
920
921 let expected_board = Board::default()
922 .make_move_new(ChessMove::from_str("e2e4").unwrap())
923 .make_move_new(ChessMove::from_str("e7e5").unwrap());
924
925 assert_eq!(engine.board, expected_board);
926 }
927
928 #[test]
929 fn test_option_setting() {
930 let mut engine = UCIEngine::new();
931 engine.set_option("Pattern_Weight", "80");
932
933 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("Pattern_Weight") {
934 assert_eq!(*value, 80);
935 } else {
936 panic!("Option not found or wrong type");
937 }
938 }
939
940 #[test]
941 fn test_debug_toggle() {
942 let mut engine = UCIEngine::new();
943
944 engine.handle_debug(&["debug", "on"]);
945 assert!(engine.debug);
946
947 engine.handle_debug(&["debug", "off"]);
948 assert!(!engine.debug);
949 }
950
951 #[test]
952 fn test_pondering_option() {
953 let engine = UCIEngine::new();
954
955 if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
957 assert!(*value); } else {
959 panic!("Ponder option should be available");
960 }
961 }
962
963 #[test]
964 fn test_ponder_command_parsing() {
965 let mut engine = UCIEngine::new();
966
967 let response = engine.process_command("go ponder");
969 assert_eq!(response, ""); }
974
975 #[test]
976 fn test_ponderhit_command() {
977 let mut engine = UCIEngine::new();
978
979 engine.pondering = true;
981 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
982
983 let response = engine.handle_ponderhit();
985 assert_eq!(response, "");
986
987 assert!(!engine.pondering);
989 assert!(engine.ponder_move.is_none());
990 }
991
992 #[test]
993 fn test_stop_during_pondering() {
994 let mut engine = UCIEngine::new();
995
996 engine.pondering = true;
998 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
999
1000 let response = engine.handle_stop();
1002 assert_eq!(response, "");
1003
1004 assert!(!engine.pondering);
1006 assert!(!engine.thinking);
1007 assert!(engine.ponder_move.is_none());
1008 }
1009
1010 #[test]
1011 fn test_expected_opponent_move() {
1012 let mut engine = UCIEngine::new();
1013
1014 let expected_move = engine.get_expected_opponent_move();
1016 assert!(expected_move.is_some());
1017
1018 let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&engine.board).collect();
1020 if let Some(mv) = expected_move {
1021 assert!(legal_moves.contains(&mv));
1022 }
1023 }
1024
1025 #[test]
1026 fn test_multi_pv_option() {
1027 let engine = UCIEngine::new();
1028
1029 if let Some(UCIOption::Spin {
1031 default,
1032 min,
1033 max,
1034 value,
1035 }) = engine.options.get("MultiPV")
1036 {
1037 assert_eq!(*default, 1);
1038 assert_eq!(*min, 1);
1039 assert_eq!(*max, 10);
1040 assert_eq!(*value, 1);
1041 } else {
1042 panic!("MultiPV option should be available");
1043 }
1044 }
1045
1046 #[test]
1047 fn test_multi_pv_setting() {
1048 let mut engine = UCIEngine::new();
1049
1050 let response = engine.process_command("setoption name MultiPV value 3");
1052 assert_eq!(response, "");
1053
1054 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1056 assert_eq!(*value, 3);
1057 } else {
1058 panic!("MultiPV option should exist");
1059 }
1060 }
1061
1062 #[test]
1063 fn test_single_pv_vs_multi_pv() {
1064 let mut engine = UCIEngine::new();
1065
1066 engine.process_command("setoption name MultiPV value 1");
1071 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1072 assert_eq!(*value, 1);
1073 }
1074
1075 engine.process_command("setoption name MultiPV value 5");
1077 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1078 assert_eq!(*value, 5);
1079 }
1080 }
1081}