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(1024);
78
79 engine.enable_opening_book();
81 engine.enable_tactical_search_default();
82 engine.configure_hybrid_evaluation(HybridConfig::default());
83
84 let mut options = HashMap::new();
85
86 options.insert(
88 "Hash".to_string(),
89 UCIOption::Spin {
90 default: 128,
91 min: 1,
92 max: 2048,
93 value: 128,
94 },
95 );
96
97 options.insert(
98 "Threads".to_string(),
99 UCIOption::Spin {
100 default: 1,
101 min: 1,
102 max: 64,
103 value: 1,
104 },
105 );
106
107 options.insert(
108 "MultiPV".to_string(),
109 UCIOption::Spin {
110 default: 1,
111 min: 1,
112 max: 10,
113 value: 1,
114 },
115 );
116
117 options.insert(
119 "Pattern_Weight".to_string(),
120 UCIOption::Spin {
121 default: 60,
122 min: 0,
123 max: 100,
124 value: 60,
125 },
126 );
127
128 options.insert(
129 "Tactical_Depth".to_string(),
130 UCIOption::Spin {
131 default: 3,
132 min: 1,
133 max: 10,
134 value: 3,
135 },
136 );
137
138 options.insert(
139 "Pattern_Confidence_Threshold".to_string(),
140 UCIOption::Spin {
141 default: 75,
142 min: 0,
143 max: 100,
144 value: 75,
145 },
146 );
147
148 options.insert(
149 "Enable_LSH".to_string(),
150 UCIOption::Check {
151 default: true,
152 value: true,
153 },
154 );
155
156 options.insert(
157 "Enable_GPU".to_string(),
158 UCIOption::Check {
159 default: true,
160 value: true,
161 },
162 );
163
164 options.insert(
165 "Ponder".to_string(),
166 UCIOption::Check {
167 default: true,
168 value: true,
169 },
170 );
171
172 Self {
173 engine,
174 board: Board::default(),
175 debug: false,
176 engine_name: "Chess Vector Engine".to_string(),
177 engine_author: "Chess Vector Engine Team".to_string(),
178 options,
179 thinking: false,
180 stop_search: false,
181 pondering: false,
182 ponder_move: None,
183 ponder_board: None,
184 }
185 }
186
187 pub fn run(&mut self) {
189 let stdin = io::stdin();
190 let mut stdout = io::stdout();
191
192 for line in stdin.lock().lines() {
193 match line {
194 Ok(command) => {
195 let response = self.process_command(command.trim());
196 if !response.is_empty() {
197 let _ = writeln!(stdout, "{response}");
198 let _ = stdout.flush();
199 }
200
201 if command.trim() == "quit" {
202 break;
203 }
204 }
205 Err(e) => {
206 if self.debug {
207 let _ = writeln!(stdout, "Error reading input: {e}");
208 }
209 break;
210 }
211 }
212 }
213 }
214
215 fn process_command(&mut self, command: &str) -> String {
217 let parts: Vec<&str> = command.split_whitespace().collect();
218 if parts.is_empty() {
219 return String::new();
220 }
221
222 match parts[0] {
223 "uci" => self.handle_uci(),
224 "debug" => self.handle_debug(&parts),
225 "isready" => self.handle_isready(),
226 "setoption" => self.handle_setoption(&parts),
227 "register" => String::new(), "ucinewgame" => self.handle_ucinewgame(),
229 "position" => self.handle_position(&parts),
230 "go" => self.handle_go(&parts),
231 "stop" => self.handle_stop(),
232 "ponderhit" => self.handle_ponderhit(),
233 "quit" => String::new(),
234 _ => {
235 if self.debug {
236 format!("Unknown command: {command}")
237 } else {
238 String::new()
239 }
240 }
241 }
242 }
243
244 fn handle_uci(&self) -> String {
245 let mut response = String::new();
246 response.push_str(&format!("id name {}\n", self.engine_name));
247 response.push_str(&format!("id author {}\n", self.engine_author));
248
249 for (name, option) in &self.options {
251 match option {
252 UCIOption::Check { default, .. } => {
253 response.push_str(&format!(
254 "option name {} type check default {}\n",
255 name, default
256 ));
257 }
258 UCIOption::Spin {
259 default, min, max, ..
260 } => {
261 response.push_str(&format!(
262 "option name {} type spin default {} min {} max {}\n",
263 name, default, min, max
264 ));
265 }
266 UCIOption::Combo {
267 default, options, ..
268 } => {
269 let combo_options = options.join(" var ");
270 response.push_str(&format!(
271 "option name {} type combo default {} var {}\n",
272 name, default, combo_options
273 ));
274 }
275 UCIOption::Button => {
276 response.push_str(&format!("option name {} type button\n", name));
277 }
278 UCIOption::String { default, .. } => {
279 response.push_str(&format!(
280 "option name {} type string default {}\n",
281 name, default
282 ));
283 }
284 }
285 }
286
287 response.push_str("uciok");
288 response
289 }
290
291 fn handle_debug(&mut self, parts: &[&str]) -> String {
292 if parts.len() >= 2 {
293 match parts[1] {
294 "on" => self.debug = true,
295 "off" => self.debug = false,
296 _ => {}
297 }
298 }
299 String::new()
300 }
301
302 fn handle_isready(&self) -> String {
303 "readyok".to_string()
304 }
305
306 fn handle_setoption(&mut self, parts: &[&str]) -> String {
307 if parts.len() >= 4 && parts[1] == "name" {
309 let mut name_parts = Vec::new();
310 let mut value_parts = Vec::new();
311 let mut in_value = false;
312
313 for &part in &parts[2..] {
314 if part == "value" {
315 in_value = true;
316 } else if in_value {
317 value_parts.push(part);
318 } else {
319 name_parts.push(part);
320 }
321 }
322
323 let name = name_parts.join(" ");
324 let value = value_parts.join(" ");
325
326 self.set_option(&name, &value);
327 }
328
329 String::new()
330 }
331
332 fn set_option(&mut self, name: &str, value: &str) {
333 if let Some(option) = self.options.get_mut(name) {
334 match option {
335 UCIOption::Check {
336 value: ref mut val, ..
337 } => {
338 *val = value == "true";
339 }
340 UCIOption::Spin {
341 value: ref mut val,
342 min,
343 max,
344 ..
345 } => {
346 if let Ok(new_val) = value.parse::<i32>() {
347 if new_val >= *min && new_val <= *max {
348 *val = new_val;
349 }
350 }
351 }
352 UCIOption::Combo {
353 value: ref mut val,
354 options,
355 ..
356 } => {
357 if options.contains(&value.to_string()) {
358 *val = value.to_string();
359 }
360 }
361 UCIOption::String {
362 value: ref mut val, ..
363 } => {
364 *val = value.to_string();
365 }
366 UCIOption::Button => {
367 }
369 }
370 }
371
372 self.apply_options();
374 }
375
376 fn apply_options(&mut self) {
377 if let Some(UCIOption::Spin {
378 value: pattern_weight,
379 ..
380 }) = self.options.get("Pattern_Weight")
381 {
382 let weight = (*pattern_weight as f32) / 100.0;
383 let config = HybridConfig {
384 pattern_weight: weight,
385 ..HybridConfig::default()
386 };
387 self.engine.configure_hybrid_evaluation(config);
388 }
389
390 if let Some(UCIOption::Spin { value: depth, .. }) = self.options.get("Tactical_Depth") {
391 let config = TacticalConfig {
392 max_depth: *depth as u32,
393 ..TacticalConfig::default()
394 };
395 self.engine.enable_tactical_search(config);
396 }
397
398 if let Some(UCIOption::Spin {
399 value: threshold, ..
400 }) = self.options.get("Pattern_Confidence_Threshold")
401 {
402 let config = HybridConfig {
403 pattern_confidence_threshold: (*threshold as f32) / 100.0,
404 ..HybridConfig::default()
405 };
406 self.engine.configure_hybrid_evaluation(config);
407 }
408
409 if let Some(UCIOption::Check {
410 value: enable_lsh, ..
411 }) = self.options.get("Enable_LSH")
412 {
413 if *enable_lsh && !self.engine.is_lsh_enabled() {
414 self.engine.enable_lsh(8, 16);
415 }
416 }
417 }
418
419 fn handle_ucinewgame(&mut self) -> String {
420 self.board = Board::default();
421 String::new()
423 }
424
425 fn handle_position(&mut self, parts: &[&str]) -> String {
426 if parts.len() < 2 {
427 return String::new();
428 }
429
430 let mut board = if parts[1] == "startpos" {
431 Board::default()
432 } else if parts[1] == "fen" && parts.len() >= 8 {
433 let fen = parts[2..8].join(" ");
434 match Board::from_str(&fen) {
435 Ok(board) => board,
436 Err(_) => {
437 if self.debug {
438 return "info string Invalid FEN".to_string();
439 }
440 return String::new();
441 }
442 }
443 } else {
444 return String::new();
445 };
446
447 if let Some(moves_idx) = parts.iter().position(|&x| x == "moves") {
449 for move_str in &parts[moves_idx + 1..] {
450 if let Ok(chess_move) = ChessMove::from_str(move_str) {
451 if board.legal(chess_move) {
452 board = board.make_move_new(chess_move);
453 } else if self.debug {
454 return format!("info string Illegal move: {move_str}");
455 }
456 }
457 }
458 }
459
460 self.board = board;
461 String::new()
462 }
463
464 fn handle_go(&mut self, parts: &[&str]) -> String {
465 if self.thinking {
466 return String::new();
467 }
468
469 let mut wtime = None;
471 let mut btime = None;
472 let mut _winc: Option<u64> = None;
473 let mut _binc: Option<u64> = None;
474 let mut movestogo = None;
475 let mut depth = None;
476 let mut _nodes: Option<u64> = None;
477 let mut movetime = None;
478 let mut infinite = false;
479 let mut ponder = false;
480
481 let mut i = 1;
482 while i < parts.len() {
483 match parts[i] {
484 "wtime" if i + 1 < parts.len() => {
485 wtime = parts[i + 1].parse().ok();
486 i += 2;
487 }
488 "btime" if i + 1 < parts.len() => {
489 btime = parts[i + 1].parse().ok();
490 i += 2;
491 }
492 "winc" if i + 1 < parts.len() => {
493 _winc = parts[i + 1].parse().ok();
494 i += 2;
495 }
496 "binc" if i + 1 < parts.len() => {
497 _binc = parts[i + 1].parse().ok();
498 i += 2;
499 }
500 "movestogo" if i + 1 < parts.len() => {
501 movestogo = parts[i + 1].parse().ok();
502 i += 2;
503 }
504 "depth" if i + 1 < parts.len() => {
505 depth = parts[i + 1].parse().ok();
506 i += 2;
507 }
508 "nodes" if i + 1 < parts.len() => {
509 _nodes = parts[i + 1].parse().ok();
510 i += 2;
511 }
512 "movetime" if i + 1 < parts.len() => {
513 movetime = parts[i + 1].parse().ok();
514 i += 2;
515 }
516 "infinite" => {
517 infinite = true;
518 i += 1;
519 }
520 "ponder" => {
521 ponder = true;
522 i += 1;
523 }
524 _ => i += 1,
525 }
526 }
527
528 let search_time = if let Some(mt) = movetime {
530 Duration::from_millis(mt)
531 } else if infinite {
532 Duration::from_secs(3600) } else {
534 let our_time = if self.board.side_to_move() == Color::White {
536 wtime.unwrap_or(30000)
537 } else {
538 btime.unwrap_or(30000)
539 };
540
541 let moves_left = movestogo.unwrap_or(30);
542 let time_per_move = our_time / moves_left.max(1);
543 Duration::from_millis(time_per_move.min(our_time / 2))
544 };
545
546 if ponder {
548 self.start_ponder_search(search_time, depth);
549 } else {
550 self.start_search(search_time, depth);
551 }
552
553 String::new()
554 }
555
556 fn start_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
557 self.thinking = true;
558 self.stop_search = false;
559
560 let board = self.board;
561 let mut engine = self.engine.clone(); let start_time = Instant::now();
563
564 let multi_pv = if let Some(UCIOption::Spin { value, .. }) = self.options.get("MultiPV") {
566 *value as usize
567 } else {
568 1
569 };
570
571 thread::spawn(move || {
573 let mut search_info = SearchInfo {
574 depth: 1,
575 seldepth: None,
576 time: 0,
577 nodes: 0,
578 nps: 0,
579 score: SearchScore::Centipawns(0),
580 pv: Vec::new(),
581 currmove: None,
582 currmovenumber: None,
583 };
584
585 let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&board).collect();
587
588 if legal_moves.is_empty() {
589 println!("bestmove 0000"); return;
591 }
592
593 let mut move_evaluations: Vec<(ChessMove, f32)> = Vec::new();
594 let mut nodes_searched = 0;
595
596 for chess_move in &legal_moves {
598 let temp_board = board.make_move_new(*chess_move);
599 nodes_searched += 1;
600
601 if let Some(position_eval) = engine.evaluate_position(&temp_board) {
603 let eval_for_us = if board.side_to_move() == chess::Color::White {
605 position_eval
606 } else {
607 -position_eval
608 };
609
610 move_evaluations.push((*chess_move, eval_for_us));
611 }
612 }
613
614 move_evaluations
616 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
617
618 let (best_move, _) = if !move_evaluations.is_empty() {
620 move_evaluations[0]
621 } else {
622 (legal_moves[0], 0.0)
624 };
625
626 search_info.time = start_time.elapsed().as_millis() as u64;
628 search_info.nodes = nodes_searched;
629 search_info.nps = if search_info.time > 0 {
630 (search_info.nodes * 1000) / search_info.time
631 } else {
632 0
633 };
634
635 let pv_count = move_evaluations.len().min(multi_pv);
637 for (pv_index, (chess_move, eval)) in move_evaluations.iter().take(pv_count).enumerate()
638 {
639 let score_cp = (*eval * 100.0) as i32;
640
641 if multi_pv == 1 {
643 println!(
645 "info depth {} score cp {} time {} nodes {} nps {} pv {}",
646 search_info.depth,
647 score_cp,
648 search_info.time,
649 search_info.nodes,
650 search_info.nps,
651 chess_move
652 );
653 } else {
654 println!(
656 "info depth {} multipv {} score cp {} time {} nodes {} nps {} pv {}",
657 search_info.depth,
658 pv_index + 1,
659 score_cp,
660 search_info.time,
661 search_info.nodes,
662 search_info.nps,
663 chess_move
664 );
665 }
666 }
667
668 println!("bestmove {best_move}");
670 });
671 }
672
673 fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
675 if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
677 if !value {
678 return; }
680 } else {
681 return; }
683
684 self.pondering = true;
685 self.thinking = true;
686 self.stop_search = false;
687
688 if let Some(ponder_move) = self.get_expected_opponent_move() {
690 self.ponder_move = Some(ponder_move);
691 self.ponder_board = Some(self.board.make_move_new(ponder_move));
692
693 if self.debug {
694 println!("info string Pondering on {ponder_move}");
695 }
696
697 let ponder_board = self.ponder_board.unwrap();
699 let mut engine = self.engine.clone();
700 let debug = self.debug;
701
702 thread::spawn(move || {
703 if let Some(eval) = engine.evaluate_position(&ponder_board) {
705 if debug {
706 println!("info string Ponder evaluation: {:.2}", eval);
707 }
708 }
709
710 });
716 }
717 }
718
719 fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
721 use chess::MoveGen;
722
723 let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
729
730 if !legal_moves.is_empty() {
731 let mut best_move = legal_moves[0];
733 let mut best_eval = f32::NEG_INFINITY;
734
735 for chess_move in legal_moves.iter().take(5) {
736 let new_board = self.board.make_move_new(*chess_move);
738 if let Some(eval) = self.engine.evaluate_position(&new_board) {
739 let opponent_eval = -eval;
741 if opponent_eval > best_eval {
742 best_eval = opponent_eval;
743 best_move = *chess_move;
744 }
745 }
746 }
747
748 Some(best_move)
749 } else {
750 None
751 }
752 }
753
754 fn handle_ponderhit(&mut self) -> String {
756 if self.pondering {
757 self.pondering = false;
758
759 if self.debug {
760 println!("info string Ponderhit received - converting ponder to normal search");
761 }
762
763 if let Some(ponder_board) = self.ponder_board {
771 let legal_moves: Vec<ChessMove> =
772 chess::MoveGen::new_legal(&ponder_board).collect();
773 if !legal_moves.is_empty() {
774 let best_move = legal_moves[0]; thread::spawn(move || {
776 println!("bestmove {best_move}");
777 });
778 }
779 }
780
781 self.ponder_move = None;
782 self.ponder_board = None;
783 }
784
785 String::new()
786 }
787
788 fn handle_stop(&mut self) -> String {
789 self.stop_search = true;
790 self.thinking = false;
791
792 if self.pondering {
794 self.pondering = false;
795 self.ponder_move = None;
796 self.ponder_board = None;
797
798 if self.debug {
799 println!("info string Pondering stopped");
800 }
801 }
802
803 String::new()
804 }
805}
806
807impl Default for UCIEngine {
808 fn default() -> Self {
809 Self::new()
810 }
811}
812
813#[derive(Debug, Clone)]
815pub struct UCIConfig {
816 pub engine_name: String,
817 pub engine_author: String,
818 pub enable_debug: bool,
819 pub default_hash_size: i32,
820 pub default_threads: i32,
821}
822
823impl Default for UCIConfig {
824 fn default() -> Self {
825 Self {
826 engine_name: "Chess Vector Engine".to_string(),
827 engine_author: "Chess Vector Engine Team".to_string(),
828 enable_debug: false,
829 default_hash_size: 128,
830 default_threads: 1,
831 }
832 }
833}
834
835pub fn run_uci_engine() {
837 let mut engine = UCIEngine::new();
838 engine.run();
839}
840
841pub fn run_uci_engine_with_config(config: UCIConfig) {
843 let mut engine = UCIEngine::new();
844 engine.engine_name = config.engine_name;
845 engine.engine_author = config.engine_author;
846 engine.debug = config.enable_debug;
847
848 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Hash") {
850 *value = config.default_hash_size;
851 }
852 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Threads") {
853 *value = config.default_threads;
854 }
855
856 engine.run();
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use std::str::FromStr;
863
864 #[test]
865 fn test_uci_initialization() {
866 let engine = UCIEngine::new();
867 assert_eq!(engine.board, Board::default());
868 assert!(!engine.debug);
869 assert!(!engine.thinking);
870 }
871
872 #[test]
873 fn test_uci_command() {
874 let engine = UCIEngine::new();
875 let response = engine.handle_uci();
876 assert!(response.contains("id name"));
877 assert!(response.contains("id author"));
878 assert!(response.contains("uciok"));
879 }
880
881 #[test]
882 fn test_isready_command() {
883 let engine = UCIEngine::new();
884 let response = engine.handle_isready();
885 assert_eq!(response, "readyok");
886 }
887
888 #[test]
889 fn test_position_startpos() {
890 let mut engine = UCIEngine::new();
891 let parts = vec!["position", "startpos"];
892 engine.handle_position(&parts);
893 assert_eq!(engine.board, Board::default());
894 }
895
896 #[test]
897 fn test_position_with_moves() {
898 let mut engine = UCIEngine::new();
899 let parts = vec!["position", "startpos", "moves", "e2e4", "e7e5"];
900 engine.handle_position(&parts);
901
902 let expected_board = Board::default()
903 .make_move_new(ChessMove::from_str("e2e4").unwrap())
904 .make_move_new(ChessMove::from_str("e7e5").unwrap());
905
906 assert_eq!(engine.board, expected_board);
907 }
908
909 #[test]
910 fn test_option_setting() {
911 let mut engine = UCIEngine::new();
912 engine.set_option("Pattern_Weight", "80");
913
914 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("Pattern_Weight") {
915 assert_eq!(*value, 80);
916 } else {
917 panic!("Option not found or wrong type");
918 }
919 }
920
921 #[test]
922 fn test_debug_toggle() {
923 let mut engine = UCIEngine::new();
924
925 engine.handle_debug(&["debug", "on"]);
926 assert!(engine.debug);
927
928 engine.handle_debug(&["debug", "off"]);
929 assert!(!engine.debug);
930 }
931
932 #[test]
933 fn test_pondering_option() {
934 let engine = UCIEngine::new();
935
936 if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
938 assert!(*value); } else {
940 panic!("Ponder option should be available");
941 }
942 }
943
944 #[test]
945 fn test_ponder_command_parsing() {
946 let mut engine = UCIEngine::new();
947
948 let response = engine.process_command("go ponder");
950 assert_eq!(response, ""); }
955
956 #[test]
957 fn test_ponderhit_command() {
958 let mut engine = UCIEngine::new();
959
960 engine.pondering = true;
962 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
963
964 let response = engine.handle_ponderhit();
966 assert_eq!(response, "");
967
968 assert!(!engine.pondering);
970 assert!(engine.ponder_move.is_none());
971 }
972
973 #[test]
974 fn test_stop_during_pondering() {
975 let mut engine = UCIEngine::new();
976
977 engine.pondering = true;
979 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
980
981 let response = engine.handle_stop();
983 assert_eq!(response, "");
984
985 assert!(!engine.pondering);
987 assert!(!engine.thinking);
988 assert!(engine.ponder_move.is_none());
989 }
990
991 #[test]
992 fn test_expected_opponent_move() {
993 let mut engine = UCIEngine::new();
994
995 let expected_move = engine.get_expected_opponent_move();
997 assert!(expected_move.is_some());
998
999 let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&engine.board).collect();
1001 if let Some(mv) = expected_move {
1002 assert!(legal_moves.contains(&mv));
1003 }
1004 }
1005
1006 #[test]
1007 fn test_multi_pv_option() {
1008 let engine = UCIEngine::new();
1009
1010 if let Some(UCIOption::Spin {
1012 default,
1013 min,
1014 max,
1015 value,
1016 }) = engine.options.get("MultiPV")
1017 {
1018 assert_eq!(*default, 1);
1019 assert_eq!(*min, 1);
1020 assert_eq!(*max, 10);
1021 assert_eq!(*value, 1);
1022 } else {
1023 panic!("MultiPV option should be available");
1024 }
1025 }
1026
1027 #[test]
1028 fn test_multi_pv_setting() {
1029 let mut engine = UCIEngine::new();
1030
1031 let response = engine.process_command("setoption name MultiPV value 3");
1033 assert_eq!(response, "");
1034
1035 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1037 assert_eq!(*value, 3);
1038 } else {
1039 panic!("MultiPV option should exist");
1040 }
1041 }
1042
1043 #[test]
1044 fn test_single_pv_vs_multi_pv() {
1045 let mut engine = UCIEngine::new();
1046
1047 engine.process_command("setoption name MultiPV value 1");
1052 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1053 assert_eq!(*value, 1);
1054 }
1055
1056 engine.process_command("setoption name MultiPV value 5");
1058 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1059 assert_eq!(*value, 5);
1060 }
1061 }
1062}