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 {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        // Parse: setoption name <name> value <value>
304        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                    // Handle button press
364                }
365            }
366        }
367
368        // Apply engine-specific options
369        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        // Could reset engine state here if needed
418        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        // Apply moves if present
444        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        // Parse go command parameters
466        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        // Calculate search time
525        let search_time = if let Some(mt) = movetime {
526            Duration::from_millis(mt)
527        } else if infinite {
528            Duration::from_secs(3600) // 1 hour max
529        } else {
530            // Simple time management
531            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        // Start search in separate thread
543        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(); // Clone the engine for threaded evaluation
558        let start_time = Instant::now();
559
560        // Get MultiPV setting
561        let multi_pv = if let Some(UCIOption::Spin { value, .. }) = self.options.get("MultiPV") {
562            *value as usize
563        } else {
564            1
565        };
566
567        // Spawn search thread
568        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            // PROPER CHESS ENGINE SEARCH: evaluate all legal moves using hybrid system
582            let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&board).collect();
583
584            if legal_moves.is_empty() {
585                println!("bestmove 0000"); // No legal moves - game over
586                return;
587            }
588
589            let mut move_evaluations: Vec<(ChessMove, f32)> = Vec::new();
590            let mut nodes_searched = 0;
591
592            // Evaluate each legal move by making it and evaluating the resulting position
593            for chess_move in &legal_moves {
594                let temp_board = board.make_move_new(*chess_move);
595                nodes_searched += 1;
596
597                // Use the engine's hybrid evaluation system (opening book + patterns + tactical)
598                if let Some(position_eval) = engine.evaluate_position(&temp_board) {
599                    // Flip evaluation for opponent's perspective
600                    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            // Sort moves by evaluation (best first)
611            move_evaluations
612                .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
613
614            // Get the best move for the final output
615            let (best_move, _) = if !move_evaluations.is_empty() {
616                move_evaluations[0]
617            } else {
618                // Fallback: no evaluations worked, use first legal move
619                (legal_moves[0], 0.0)
620            };
621
622            // Update common search info
623            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            // Output multiple PV lines
632            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                // Send UCI info for this PV line
638                if multi_pv == 1 {
639                    // Single PV (standard format)
640                    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                    // Multi-PV format (include multipv field)
651                    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            // Send best move
665            println!("bestmove {best_move}");
666        });
667    }
668
669    /// Start pondering search (thinking on opponent's time)
670    fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
671        // Check if pondering is enabled
672        if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
673            if !value {
674                return; // Pondering disabled
675            }
676        } else {
677            return; // No ponder option
678        }
679
680        self.pondering = true;
681        self.thinking = true;
682        self.stop_search = false;
683
684        // Find a likely opponent move to ponder on
685        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            // Start pondering in background thread
694            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                // Perform deep search on pondered position
700                if let Some(eval) = engine.evaluate_position(&ponder_board) {
701                    if debug {
702                        println!("info string Ponder evaluation: {eval:.2}");
703                    }
704                }
705
706                // In a full implementation, we would:
707                // 1. Run full tactical search on ponder position
708                // 2. Analyze multiple candidate moves
709                // 3. Store results for quick retrieval on ponderhit
710                // 4. Continue until stop or ponderhit
711            });
712        }
713    }
714
715    /// Get the most likely opponent move for pondering
716    fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
717        use chess::MoveGen;
718
719        // Simple strategy: pick the first legal move
720        // In a real engine, this would use:
721        // - Principal variation from previous search
722        // - Opening book moves
723        // - Most common moves in this position
724        let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
725
726        if !legal_moves.is_empty() {
727            // Use engine evaluation to pick most likely move
728            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                // Check top 5 moves
733                let new_board = self.board.make_move_new(*chess_move);
734                if let Some(eval) = self.engine.evaluate_position(&new_board) {
735                    // From opponent's perspective (flip evaluation)
736                    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    /// Handle ponderhit command (opponent played the expected move)
751    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            // Convert pondering to normal search
760            // In a full implementation, we would:
761            // 1. Use cached ponder results
762            // 2. Continue search with time limits
763            // 3. Output bestmove when search completes
764
765            // For now, just output a quick result
766            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]; // Simple fallback
771                    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        // Stop pondering if active
789        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/// Configuration for the UCI engine
810#[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
831/// Run the UCI engine with default configuration
832pub fn run_uci_engine() {
833    let mut engine = UCIEngine::new();
834    engine.run();
835}
836
837/// Run the UCI engine with custom configuration
838pub 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    // Apply configuration to options
845    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        // Check that Ponder option exists and is enabled by default
933        if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
934            assert!(*value); // Should be enabled by default
935        } 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        // Test "go ponder" command
945        let response = engine.process_command("go ponder");
946        assert_eq!(response, ""); // Should return empty string
947
948        // Pondering should be active (may be set asynchronously)
949        // We don't assert on engine.pondering here due to threading
950    }
951
952    #[test]
953    fn test_ponderhit_command() {
954        let mut engine = UCIEngine::new();
955
956        // Start pondering first
957        engine.pondering = true;
958        engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
959
960        // Send ponderhit
961        let response = engine.handle_ponderhit();
962        assert_eq!(response, "");
963
964        // Pondering should be stopped
965        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        // Start pondering
974        engine.pondering = true;
975        engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
976
977        // Send stop command
978        let response = engine.handle_stop();
979        assert_eq!(response, "");
980
981        // Pondering should be stopped
982        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        // Test getting expected opponent move from starting position
992        let expected_move = engine.get_expected_opponent_move();
993        assert!(expected_move.is_some());
994
995        // Should be a legal move
996        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        // Check that MultiPV option exists and has correct default
1007        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        // Test setting MultiPV option
1028        let response = engine.process_command("setoption name MultiPV value 3");
1029        assert_eq!(response, "");
1030
1031        // Check that option was set
1032        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        // Test that engine accepts both single and multi-PV modes
1044        // (We can't easily test the actual output format in unit tests due to threading)
1045
1046        // Set to single PV
1047        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        // Set to multi-PV
1053        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}