chess_vector_engine/
uci.rs

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
10/// UCI (Universal Chess Interface) protocol implementation for the chess vector engine
11pub 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/// UCI option types
26#[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/// Search information for UCI info command
51#[derive(Debug, Clone)]
52pub struct SearchInfo {
53    depth: u32,
54    #[allow(dead_code)]
55    seldepth: Option<u32>,
56    time: u64, // milliseconds
57    nodes: u64,
58    nps: u64, // nodes per second
59    #[allow(dead_code)]
60    score: SearchScore,
61    #[allow(dead_code)]
62    pv: Vec<ChessMove>, // principal variation
63    #[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), // moves to mate (positive if we're winning)
73}
74
75impl UCIEngine {
76    pub fn new() -> Self {
77        let mut engine = ChessVectorEngine::new(1024);
78
79        // Enable all advanced features for UCI
80        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        // Standard UCI options
87        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        // Chess Vector Engine specific options
118        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    /// Main UCI loop
188    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    /// Process UCI command and return response
216    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(), // Not implemented
228            "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        // Send options
250        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        // Parse: setoption name <name> value <value>
308        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                    // Handle button press
368                }
369            }
370        }
371
372        // Apply engine-specific options
373        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        // Could reset engine state here if needed
422        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        // Apply moves if present
448        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        // Parse go command parameters
470        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        // Calculate search time
529        let search_time = if let Some(mt) = movetime {
530            Duration::from_millis(mt)
531        } else if infinite {
532            Duration::from_secs(3600) // 1 hour max
533        } else {
534            // Simple time management
535            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        // Start search in separate thread
547        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(); // Clone the engine for threaded evaluation
562        let start_time = Instant::now();
563
564        // Get MultiPV setting
565        let multi_pv = if let Some(UCIOption::Spin { value, .. }) = self.options.get("MultiPV") {
566            *value as usize
567        } else {
568            1
569        };
570
571        // Spawn search thread
572        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            // PROPER CHESS ENGINE SEARCH: evaluate all legal moves using hybrid system
586            let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&board).collect();
587
588            if legal_moves.is_empty() {
589                println!("bestmove 0000"); // No legal moves - game over
590                return;
591            }
592
593            let mut move_evaluations: Vec<(ChessMove, f32)> = Vec::new();
594            let mut nodes_searched = 0;
595
596            // Evaluate each legal move by making it and evaluating the resulting position
597            for chess_move in &legal_moves {
598                let temp_board = board.make_move_new(*chess_move);
599                nodes_searched += 1;
600
601                // Use the engine's hybrid evaluation system (opening book + patterns + tactical)
602                if let Some(position_eval) = engine.evaluate_position(&temp_board) {
603                    // Flip evaluation for opponent's perspective
604                    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            // Sort moves by evaluation (best first)
615            move_evaluations
616                .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
617
618            // Get the best move for the final output
619            let (best_move, _) = if !move_evaluations.is_empty() {
620                move_evaluations[0]
621            } else {
622                // Fallback: no evaluations worked, use first legal move
623                (legal_moves[0], 0.0)
624            };
625
626            // Update common search info
627            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            // Output multiple PV lines
636            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                // Send UCI info for this PV line
642                if multi_pv == 1 {
643                    // Single PV (standard format)
644                    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                    // Multi-PV format (include multipv field)
655                    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            // Send best move
669            println!("bestmove {best_move}");
670        });
671    }
672
673    /// Start pondering search (thinking on opponent's time)
674    fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
675        // Check if pondering is enabled
676        if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
677            if !value {
678                return; // Pondering disabled
679            }
680        } else {
681            return; // No ponder option
682        }
683
684        self.pondering = true;
685        self.thinking = true;
686        self.stop_search = false;
687
688        // Find a likely opponent move to ponder on
689        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            // Start pondering in background thread
698            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                // Perform deep search on pondered position
704                if let Some(eval) = engine.evaluate_position(&ponder_board) {
705                    if debug {
706                        println!("info string Ponder evaluation: {:.2}", eval);
707                    }
708                }
709
710                // In a full implementation, we would:
711                // 1. Run full tactical search on ponder position
712                // 2. Analyze multiple candidate moves
713                // 3. Store results for quick retrieval on ponderhit
714                // 4. Continue until stop or ponderhit
715            });
716        }
717    }
718
719    /// Get the most likely opponent move for pondering
720    fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
721        use chess::MoveGen;
722
723        // Simple strategy: pick the first legal move
724        // In a real engine, this would use:
725        // - Principal variation from previous search
726        // - Opening book moves
727        // - Most common moves in this position
728        let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
729
730        if !legal_moves.is_empty() {
731            // Use engine evaluation to pick most likely move
732            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                // Check top 5 moves
737                let new_board = self.board.make_move_new(*chess_move);
738                if let Some(eval) = self.engine.evaluate_position(&new_board) {
739                    // From opponent's perspective (flip evaluation)
740                    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    /// Handle ponderhit command (opponent played the expected move)
755    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            // Convert pondering to normal search
764            // In a full implementation, we would:
765            // 1. Use cached ponder results
766            // 2. Continue search with time limits
767            // 3. Output bestmove when search completes
768
769            // For now, just output a quick result
770            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]; // Simple fallback
775                    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        // Stop pondering if active
793        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/// Configuration for the UCI engine
814#[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
835/// Run the UCI engine with default configuration
836pub fn run_uci_engine() {
837    let mut engine = UCIEngine::new();
838    engine.run();
839}
840
841/// Run the UCI engine with custom configuration
842pub 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    // Apply configuration to options
849    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        // Check that Ponder option exists and is enabled by default
937        if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
938            assert!(*value); // Should be enabled by default
939        } 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        // Test "go ponder" command
949        let response = engine.process_command("go ponder");
950        assert_eq!(response, ""); // Should return empty string
951
952        // Pondering should be active (may be set asynchronously)
953        // We don't assert on engine.pondering here due to threading
954    }
955
956    #[test]
957    fn test_ponderhit_command() {
958        let mut engine = UCIEngine::new();
959
960        // Start pondering first
961        engine.pondering = true;
962        engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
963
964        // Send ponderhit
965        let response = engine.handle_ponderhit();
966        assert_eq!(response, "");
967
968        // Pondering should be stopped
969        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        // Start pondering
978        engine.pondering = true;
979        engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
980
981        // Send stop command
982        let response = engine.handle_stop();
983        assert_eq!(response, "");
984
985        // Pondering should be stopped
986        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        // Test getting expected opponent move from starting position
996        let expected_move = engine.get_expected_opponent_move();
997        assert!(expected_move.is_some());
998
999        // Should be a legal move
1000        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        // Check that MultiPV option exists and has correct default
1011        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        // Test setting MultiPV option
1032        let response = engine.process_command("setoption name MultiPV value 3");
1033        assert_eq!(response, "");
1034
1035        // Check that option was set
1036        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        // Test that engine accepts both single and multi-PV modes
1048        // (We can't easily test the actual output format in unit tests due to threading)
1049
1050        // Set to single PV
1051        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        // Set to multi-PV
1057        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}