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 {name} type check default {default}\n"
255 ));
256 }
257 UCIOption::Spin {
258 default, min, max, ..
259 } => {
260 response.push_str(&format!(
261 "option name {name} type spin default {default} min {min} max {max}\n"
262 ));
263 }
264 UCIOption::Combo {
265 default, options, ..
266 } => {
267 let combo_options = options.join(" var ");
268 response.push_str(&format!(
269 "option name {name} type combo default {default} var {combo_options}\n"
270 ));
271 }
272 UCIOption::Button => {
273 response.push_str(&format!("option name {name} type button\n"));
274 }
275 UCIOption::String { default, .. } => {
276 response.push_str(&format!(
277 "option name {name} type string default {default}\n"
278 ));
279 }
280 }
281 }
282
283 response.push_str("uciok");
284 response
285 }
286
287 fn handle_debug(&mut self, parts: &[&str]) -> String {
288 if parts.len() >= 2 {
289 match parts[1] {
290 "on" => self.debug = true,
291 "off" => self.debug = false,
292 _ => {}
293 }
294 }
295 String::new()
296 }
297
298 fn handle_isready(&self) -> String {
299 "readyok".to_string()
300 }
301
302 fn handle_setoption(&mut self, parts: &[&str]) -> String {
303 if parts.len() >= 4 && parts[1] == "name" {
305 let mut name_parts = Vec::new();
306 let mut value_parts = Vec::new();
307 let mut in_value = false;
308
309 for &part in &parts[2..] {
310 if part == "value" {
311 in_value = true;
312 } else if in_value {
313 value_parts.push(part);
314 } else {
315 name_parts.push(part);
316 }
317 }
318
319 let name = name_parts.join(" ");
320 let value = value_parts.join(" ");
321
322 self.set_option(&name, &value);
323 }
324
325 String::new()
326 }
327
328 fn set_option(&mut self, name: &str, value: &str) {
329 if let Some(option) = self.options.get_mut(name) {
330 match option {
331 UCIOption::Check {
332 value: ref mut val, ..
333 } => {
334 *val = value == "true";
335 }
336 UCIOption::Spin {
337 value: ref mut val,
338 min,
339 max,
340 ..
341 } => {
342 if let Ok(new_val) = value.parse::<i32>() {
343 if new_val >= *min && new_val <= *max {
344 *val = new_val;
345 }
346 }
347 }
348 UCIOption::Combo {
349 value: ref mut val,
350 options,
351 ..
352 } => {
353 if options.contains(&value.to_string()) {
354 *val = value.to_string();
355 }
356 }
357 UCIOption::String {
358 value: ref mut val, ..
359 } => {
360 *val = value.to_string();
361 }
362 UCIOption::Button => {
363 }
365 }
366 }
367
368 self.apply_options();
370 }
371
372 fn apply_options(&mut self) {
373 if let Some(UCIOption::Spin {
374 value: pattern_weight,
375 ..
376 }) = self.options.get("Pattern_Weight")
377 {
378 let weight = (*pattern_weight as f32) / 100.0;
379 let config = HybridConfig {
380 pattern_weight: weight,
381 ..HybridConfig::default()
382 };
383 self.engine.configure_hybrid_evaluation(config);
384 }
385
386 if let Some(UCIOption::Spin { value: depth, .. }) = self.options.get("Tactical_Depth") {
387 let config = TacticalConfig {
388 max_depth: *depth as u32,
389 ..TacticalConfig::default()
390 };
391 self.engine.enable_tactical_search(config);
392 }
393
394 if let Some(UCIOption::Spin {
395 value: threshold, ..
396 }) = self.options.get("Pattern_Confidence_Threshold")
397 {
398 let config = HybridConfig {
399 pattern_confidence_threshold: (*threshold as f32) / 100.0,
400 ..HybridConfig::default()
401 };
402 self.engine.configure_hybrid_evaluation(config);
403 }
404
405 if let Some(UCIOption::Check {
406 value: enable_lsh, ..
407 }) = self.options.get("Enable_LSH")
408 {
409 if *enable_lsh && !self.engine.is_lsh_enabled() {
410 self.engine.enable_lsh(8, 16);
411 }
412 }
413 }
414
415 fn handle_ucinewgame(&mut self) -> String {
416 self.board = Board::default();
417 String::new()
419 }
420
421 fn handle_position(&mut self, parts: &[&str]) -> String {
422 if parts.len() < 2 {
423 return String::new();
424 }
425
426 let mut board = if parts[1] == "startpos" {
427 Board::default()
428 } else if parts[1] == "fen" && parts.len() >= 8 {
429 let fen = parts[2..8].join(" ");
430 match Board::from_str(&fen) {
431 Ok(board) => board,
432 Err(_) => {
433 if self.debug {
434 return "info string Invalid FEN".to_string();
435 }
436 return String::new();
437 }
438 }
439 } else {
440 return String::new();
441 };
442
443 if let Some(moves_idx) = parts.iter().position(|&x| x == "moves") {
445 for move_str in &parts[moves_idx + 1..] {
446 if let Ok(chess_move) = ChessMove::from_str(move_str) {
447 if board.legal(chess_move) {
448 board = board.make_move_new(chess_move);
449 } else if self.debug {
450 return format!("info string Illegal move: {move_str}");
451 }
452 }
453 }
454 }
455
456 self.board = board;
457 String::new()
458 }
459
460 fn handle_go(&mut self, parts: &[&str]) -> String {
461 if self.thinking {
462 return String::new();
463 }
464
465 let mut wtime = None;
467 let mut btime = None;
468 let mut _winc: Option<u64> = None;
469 let mut _binc: Option<u64> = None;
470 let mut movestogo = None;
471 let mut depth = None;
472 let mut _nodes: Option<u64> = None;
473 let mut movetime = None;
474 let mut infinite = false;
475 let mut ponder = false;
476
477 let mut i = 1;
478 while i < parts.len() {
479 match parts[i] {
480 "wtime" if i + 1 < parts.len() => {
481 wtime = parts[i + 1].parse().ok();
482 i += 2;
483 }
484 "btime" if i + 1 < parts.len() => {
485 btime = parts[i + 1].parse().ok();
486 i += 2;
487 }
488 "winc" if i + 1 < parts.len() => {
489 _winc = parts[i + 1].parse().ok();
490 i += 2;
491 }
492 "binc" if i + 1 < parts.len() => {
493 _binc = parts[i + 1].parse().ok();
494 i += 2;
495 }
496 "movestogo" if i + 1 < parts.len() => {
497 movestogo = parts[i + 1].parse().ok();
498 i += 2;
499 }
500 "depth" if i + 1 < parts.len() => {
501 depth = parts[i + 1].parse().ok();
502 i += 2;
503 }
504 "nodes" if i + 1 < parts.len() => {
505 _nodes = parts[i + 1].parse().ok();
506 i += 2;
507 }
508 "movetime" if i + 1 < parts.len() => {
509 movetime = parts[i + 1].parse().ok();
510 i += 2;
511 }
512 "infinite" => {
513 infinite = true;
514 i += 1;
515 }
516 "ponder" => {
517 ponder = true;
518 i += 1;
519 }
520 _ => i += 1,
521 }
522 }
523
524 let search_time = if let Some(mt) = movetime {
526 Duration::from_millis(mt)
527 } else if infinite {
528 Duration::from_secs(3600) } else {
530 let our_time = if self.board.side_to_move() == Color::White {
532 wtime.unwrap_or(30000)
533 } else {
534 btime.unwrap_or(30000)
535 };
536
537 let moves_left = movestogo.unwrap_or(30);
538 let time_per_move = our_time / moves_left.max(1);
539 Duration::from_millis(time_per_move.min(our_time / 2))
540 };
541
542 if ponder {
544 self.start_ponder_search(search_time, depth);
545 } else {
546 self.start_search(search_time, depth);
547 }
548
549 String::new()
550 }
551
552 fn start_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
553 self.thinking = true;
554 self.stop_search = false;
555
556 let board = self.board;
557 let mut engine = self.engine.clone(); let start_time = Instant::now();
559
560 let multi_pv = if let Some(UCIOption::Spin { value, .. }) = self.options.get("MultiPV") {
562 *value as usize
563 } else {
564 1
565 };
566
567 thread::spawn(move || {
569 let mut search_info = SearchInfo {
570 depth: 1,
571 seldepth: None,
572 time: 0,
573 nodes: 0,
574 nps: 0,
575 score: SearchScore::Centipawns(0),
576 pv: Vec::new(),
577 currmove: None,
578 currmovenumber: None,
579 };
580
581 let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&board).collect();
583
584 if legal_moves.is_empty() {
585 println!("bestmove 0000"); return;
587 }
588
589 let mut move_evaluations: Vec<(ChessMove, f32)> = Vec::new();
590 let mut nodes_searched = 0;
591
592 for chess_move in &legal_moves {
594 let temp_board = board.make_move_new(*chess_move);
595 nodes_searched += 1;
596
597 if let Some(position_eval) = engine.evaluate_position(&temp_board) {
599 let eval_for_us = if board.side_to_move() == chess::Color::White {
601 position_eval
602 } else {
603 -position_eval
604 };
605
606 move_evaluations.push((*chess_move, eval_for_us));
607 }
608 }
609
610 move_evaluations
612 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
613
614 let (best_move, _) = if !move_evaluations.is_empty() {
616 move_evaluations[0]
617 } else {
618 (legal_moves[0], 0.0)
620 };
621
622 search_info.time = start_time.elapsed().as_millis() as u64;
624 search_info.nodes = nodes_searched;
625 search_info.nps = if search_info.time > 0 {
626 (search_info.nodes * 1000) / search_info.time
627 } else {
628 0
629 };
630
631 let pv_count = move_evaluations.len().min(multi_pv);
633 for (pv_index, (chess_move, eval)) in move_evaluations.iter().take(pv_count).enumerate()
634 {
635 let score_cp = (*eval * 100.0) as i32;
636
637 if multi_pv == 1 {
639 println!(
641 "info depth {} score cp {} time {} nodes {} nps {} pv {}",
642 search_info.depth,
643 score_cp,
644 search_info.time,
645 search_info.nodes,
646 search_info.nps,
647 chess_move
648 );
649 } else {
650 println!(
652 "info depth {} multipv {} score cp {} time {} nodes {} nps {} pv {}",
653 search_info.depth,
654 pv_index + 1,
655 score_cp,
656 search_info.time,
657 search_info.nodes,
658 search_info.nps,
659 chess_move
660 );
661 }
662 }
663
664 println!("bestmove {best_move}");
666 });
667 }
668
669 fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
671 if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
673 if !value {
674 return; }
676 } else {
677 return; }
679
680 self.pondering = true;
681 self.thinking = true;
682 self.stop_search = false;
683
684 if let Some(ponder_move) = self.get_expected_opponent_move() {
686 self.ponder_move = Some(ponder_move);
687 self.ponder_board = Some(self.board.make_move_new(ponder_move));
688
689 if self.debug {
690 println!("info string Pondering on {ponder_move}");
691 }
692
693 let ponder_board = self.ponder_board.unwrap();
695 let mut engine = self.engine.clone();
696 let debug = self.debug;
697
698 thread::spawn(move || {
699 if let Some(eval) = engine.evaluate_position(&ponder_board) {
701 if debug {
702 println!("info string Ponder evaluation: {eval:.2}");
703 }
704 }
705
706 });
712 }
713 }
714
715 fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
717 use chess::MoveGen;
718
719 let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
725
726 if !legal_moves.is_empty() {
727 let mut best_move = legal_moves[0];
729 let mut best_eval = f32::NEG_INFINITY;
730
731 for chess_move in legal_moves.iter().take(5) {
732 let new_board = self.board.make_move_new(*chess_move);
734 if let Some(eval) = self.engine.evaluate_position(&new_board) {
735 let opponent_eval = -eval;
737 if opponent_eval > best_eval {
738 best_eval = opponent_eval;
739 best_move = *chess_move;
740 }
741 }
742 }
743
744 Some(best_move)
745 } else {
746 None
747 }
748 }
749
750 fn handle_ponderhit(&mut self) -> String {
752 if self.pondering {
753 self.pondering = false;
754
755 if self.debug {
756 println!("info string Ponderhit received - converting ponder to normal search");
757 }
758
759 if let Some(ponder_board) = self.ponder_board {
767 let legal_moves: Vec<ChessMove> =
768 chess::MoveGen::new_legal(&ponder_board).collect();
769 if !legal_moves.is_empty() {
770 let best_move = legal_moves[0]; thread::spawn(move || {
772 println!("bestmove {best_move}");
773 });
774 }
775 }
776
777 self.ponder_move = None;
778 self.ponder_board = None;
779 }
780
781 String::new()
782 }
783
784 fn handle_stop(&mut self) -> String {
785 self.stop_search = true;
786 self.thinking = false;
787
788 if self.pondering {
790 self.pondering = false;
791 self.ponder_move = None;
792 self.ponder_board = None;
793
794 if self.debug {
795 println!("info string Pondering stopped");
796 }
797 }
798
799 String::new()
800 }
801}
802
803impl Default for UCIEngine {
804 fn default() -> Self {
805 Self::new()
806 }
807}
808
809#[derive(Debug, Clone)]
811pub struct UCIConfig {
812 pub engine_name: String,
813 pub engine_author: String,
814 pub enable_debug: bool,
815 pub default_hash_size: i32,
816 pub default_threads: i32,
817}
818
819impl Default for UCIConfig {
820 fn default() -> Self {
821 Self {
822 engine_name: "Chess Vector Engine".to_string(),
823 engine_author: "Chess Vector Engine Team".to_string(),
824 enable_debug: false,
825 default_hash_size: 128,
826 default_threads: 1,
827 }
828 }
829}
830
831pub fn run_uci_engine() {
833 let mut engine = UCIEngine::new();
834 engine.run();
835}
836
837pub fn run_uci_engine_with_config(config: UCIConfig) {
839 let mut engine = UCIEngine::new();
840 engine.engine_name = config.engine_name;
841 engine.engine_author = config.engine_author;
842 engine.debug = config.enable_debug;
843
844 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Hash") {
846 *value = config.default_hash_size;
847 }
848 if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Threads") {
849 *value = config.default_threads;
850 }
851
852 engine.run();
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use std::str::FromStr;
859
860 #[test]
861 fn test_uci_initialization() {
862 let engine = UCIEngine::new();
863 assert_eq!(engine.board, Board::default());
864 assert!(!engine.debug);
865 assert!(!engine.thinking);
866 }
867
868 #[test]
869 fn test_uci_command() {
870 let engine = UCIEngine::new();
871 let response = engine.handle_uci();
872 assert!(response.contains("id name"));
873 assert!(response.contains("id author"));
874 assert!(response.contains("uciok"));
875 }
876
877 #[test]
878 fn test_isready_command() {
879 let engine = UCIEngine::new();
880 let response = engine.handle_isready();
881 assert_eq!(response, "readyok");
882 }
883
884 #[test]
885 fn test_position_startpos() {
886 let mut engine = UCIEngine::new();
887 let parts = vec!["position", "startpos"];
888 engine.handle_position(&parts);
889 assert_eq!(engine.board, Board::default());
890 }
891
892 #[test]
893 fn test_position_with_moves() {
894 let mut engine = UCIEngine::new();
895 let parts = vec!["position", "startpos", "moves", "e2e4", "e7e5"];
896 engine.handle_position(&parts);
897
898 let expected_board = Board::default()
899 .make_move_new(ChessMove::from_str("e2e4").unwrap())
900 .make_move_new(ChessMove::from_str("e7e5").unwrap());
901
902 assert_eq!(engine.board, expected_board);
903 }
904
905 #[test]
906 fn test_option_setting() {
907 let mut engine = UCIEngine::new();
908 engine.set_option("Pattern_Weight", "80");
909
910 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("Pattern_Weight") {
911 assert_eq!(*value, 80);
912 } else {
913 panic!("Option not found or wrong type");
914 }
915 }
916
917 #[test]
918 fn test_debug_toggle() {
919 let mut engine = UCIEngine::new();
920
921 engine.handle_debug(&["debug", "on"]);
922 assert!(engine.debug);
923
924 engine.handle_debug(&["debug", "off"]);
925 assert!(!engine.debug);
926 }
927
928 #[test]
929 fn test_pondering_option() {
930 let engine = UCIEngine::new();
931
932 if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
934 assert!(*value); } else {
936 panic!("Ponder option should be available");
937 }
938 }
939
940 #[test]
941 fn test_ponder_command_parsing() {
942 let mut engine = UCIEngine::new();
943
944 let response = engine.process_command("go ponder");
946 assert_eq!(response, ""); }
951
952 #[test]
953 fn test_ponderhit_command() {
954 let mut engine = UCIEngine::new();
955
956 engine.pondering = true;
958 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
959
960 let response = engine.handle_ponderhit();
962 assert_eq!(response, "");
963
964 assert!(!engine.pondering);
966 assert!(engine.ponder_move.is_none());
967 }
968
969 #[test]
970 fn test_stop_during_pondering() {
971 let mut engine = UCIEngine::new();
972
973 engine.pondering = true;
975 engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
976
977 let response = engine.handle_stop();
979 assert_eq!(response, "");
980
981 assert!(!engine.pondering);
983 assert!(!engine.thinking);
984 assert!(engine.ponder_move.is_none());
985 }
986
987 #[test]
988 fn test_expected_opponent_move() {
989 let mut engine = UCIEngine::new();
990
991 let expected_move = engine.get_expected_opponent_move();
993 assert!(expected_move.is_some());
994
995 let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&engine.board).collect();
997 if let Some(mv) = expected_move {
998 assert!(legal_moves.contains(&mv));
999 }
1000 }
1001
1002 #[test]
1003 fn test_multi_pv_option() {
1004 let engine = UCIEngine::new();
1005
1006 if let Some(UCIOption::Spin {
1008 default,
1009 min,
1010 max,
1011 value,
1012 }) = engine.options.get("MultiPV")
1013 {
1014 assert_eq!(*default, 1);
1015 assert_eq!(*min, 1);
1016 assert_eq!(*max, 10);
1017 assert_eq!(*value, 1);
1018 } else {
1019 panic!("MultiPV option should be available");
1020 }
1021 }
1022
1023 #[test]
1024 fn test_multi_pv_setting() {
1025 let mut engine = UCIEngine::new();
1026
1027 let response = engine.process_command("setoption name MultiPV value 3");
1029 assert_eq!(response, "");
1030
1031 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1033 assert_eq!(*value, 3);
1034 } else {
1035 panic!("MultiPV option should exist");
1036 }
1037 }
1038
1039 #[test]
1040 fn test_single_pv_vs_multi_pv() {
1041 let mut engine = UCIEngine::new();
1042
1043 engine.process_command("setoption name MultiPV value 1");
1048 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1049 assert_eq!(*value, 1);
1050 }
1051
1052 engine.process_command("setoption name MultiPV value 5");
1054 if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1055 assert_eq!(*value, 5);
1056 }
1057 }
1058}