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