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            // Create calibrated evaluator once for efficiency
604            use crate::evaluation_calibration::CalibratedEvaluator;
605            let calibrated_evaluator = CalibratedEvaluator::new(
606                crate::evaluation_calibration::CalibrationConfig::default()
607            );
608
609            // Evaluate each legal move by making it and evaluating the resulting position
610            for chess_move in &legal_moves {
611                let temp_board = board.make_move_new(*chess_move);
612                nodes_searched += 1;
613
614                // Use calibrated evaluation for better move quality
615                let position_eval = if let Some(entry) = engine.get_opening_entry(&temp_board) {
616                    entry.evaluation
617                } else {
618                    // Use calibrated evaluator for better accuracy than basic material
619                    calibrated_evaluator.evaluate_centipawns(&temp_board) as f32 / 100.0
620                };
621                {
622                    // Flip evaluation for opponent's perspective
623                    let eval_for_us = if board.side_to_move() == chess::Color::White {
624                        position_eval
625                    } else {
626                        -position_eval
627                    };
628
629                    move_evaluations.push((*chess_move, eval_for_us));
630                }
631            }
632
633            // Sort moves by evaluation (best first)
634            move_evaluations
635                .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
636
637            // Get the best move for the final output
638            let (best_move, _) = if !move_evaluations.is_empty() {
639                move_evaluations[0]
640            } else {
641                // Fallback: no evaluations worked, use first legal move
642                (legal_moves[0], 0.0)
643            };
644
645            // Update common search info
646            search_info.time = start_time.elapsed().as_millis() as u64;
647            search_info.nodes = nodes_searched;
648            search_info.nps = if search_info.time > 0 {
649                (search_info.nodes * 1000) / search_info.time
650            } else {
651                0
652            };
653
654            // Output multiple PV lines
655            let pv_count = move_evaluations.len().min(multi_pv);
656            for (pv_index, (chess_move, eval)) in move_evaluations.iter().take(pv_count).enumerate()
657            {
658                let score_cp = (*eval * 100.0) as i32;
659
660                // Send UCI info for this PV line
661                if multi_pv == 1 {
662                    // Single PV (standard format)
663                    println!(
664                        "info depth {} score cp {} time {} nodes {} nps {} pv {}",
665                        search_info.depth,
666                        score_cp,
667                        search_info.time,
668                        search_info.nodes,
669                        search_info.nps,
670                        chess_move
671                    );
672                } else {
673                    // Multi-PV format (include multipv field)
674                    println!(
675                        "info depth {} multipv {} score cp {} time {} nodes {} nps {} pv {}",
676                        search_info.depth,
677                        pv_index + 1,
678                        score_cp,
679                        search_info.time,
680                        search_info.nodes,
681                        search_info.nps,
682                        chess_move
683                    );
684                }
685            }
686
687            // Send best move
688            println!("bestmove {best_move}");
689        });
690    }
691
692    /// Start pondering search (thinking on opponent's time)
693    fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
694        // Check if pondering is enabled
695        if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
696            if !value {
697                return; // Pondering disabled
698            }
699        } else {
700            return; // No ponder option
701        }
702
703        self.pondering = true;
704        self.thinking = true;
705        self.stop_search = false;
706
707        // Find a likely opponent move to ponder on
708        if let Some(ponder_move) = self.get_expected_opponent_move() {
709            self.ponder_move = Some(ponder_move);
710            self.ponder_board = Some(self.board.make_move_new(ponder_move));
711
712            if self.debug {
713                println!("info string Pondering on {ponder_move}");
714            }
715
716            // Start pondering in background thread
717            let ponder_board = self.ponder_board.unwrap();
718            let mut engine = self.engine.clone();
719            let debug = self.debug;
720
721            thread::spawn(move || {
722                // Perform deep search on pondered position
723                if let Some(eval) = engine.evaluate_position(&ponder_board) {
724                    if debug {
725                        println!("info string Ponder evaluation: {eval:.2}");
726                    }
727                }
728
729                // In a full implementation, we would:
730                // 1. Run full tactical search on ponder position
731                // 2. Analyze multiple candidate moves
732                // 3. Store results for quick retrieval on ponderhit
733                // 4. Continue until stop or ponderhit
734            });
735        }
736    }
737
738    /// Get the most likely opponent move for pondering
739    fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
740        use chess::MoveGen;
741
742        // Simple strategy: pick the first legal move
743        // In a real engine, this would use:
744        // - Principal variation from previous search
745        // - Opening book moves
746        // - Most common moves in this position
747        let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
748
749        if !legal_moves.is_empty() {
750            // Use engine evaluation to pick most likely move
751            let mut best_move = legal_moves[0];
752            let mut best_eval = f32::NEG_INFINITY;
753
754            for chess_move in legal_moves.iter().take(5) {
755                // Check top 5 moves
756                let new_board = self.board.make_move_new(*chess_move);
757                if let Some(eval) = self.engine.evaluate_position(&new_board) {
758                    // From opponent's perspective (flip evaluation)
759                    let opponent_eval = -eval;
760                    if opponent_eval > best_eval {
761                        best_eval = opponent_eval;
762                        best_move = *chess_move;
763                    }
764                }
765            }
766
767            Some(best_move)
768        } else {
769            None
770        }
771    }
772
773    /// Handle ponderhit command (opponent played the expected move)
774    fn handle_ponderhit(&mut self) -> String {
775        if self.pondering {
776            self.pondering = false;
777
778            if self.debug {
779                println!("info string Ponderhit received - converting ponder to normal search");
780            }
781
782            // Convert pondering to normal search
783            // In a full implementation, we would:
784            // 1. Use cached ponder results
785            // 2. Continue search with time limits
786            // 3. Output bestmove when search completes
787
788            // Evaluate the pondered position and return best move
789            if let Some(ponder_board) = self.ponder_board {
790                let legal_moves: Vec<ChessMove> =
791                    chess::MoveGen::new_legal(&ponder_board).collect();
792                if !legal_moves.is_empty() {
793                    // Use engine evaluation to find best move instead of simple fallback
794                    let mut best_move = legal_moves[0];
795                    let mut best_eval = f32::NEG_INFINITY;
796                    
797                    for chess_move in legal_moves.iter().take(5) {
798                        let new_board = ponder_board.make_move_new(*chess_move);
799                        if let Some(eval) = self.engine.evaluate_position(&new_board) {
800                            if eval > best_eval {
801                                best_eval = eval;
802                                best_move = *chess_move;
803                            }
804                        }
805                    }
806                    thread::spawn(move || {
807                        println!("bestmove {best_move}");
808                    });
809                }
810            }
811
812            self.ponder_move = None;
813            self.ponder_board = None;
814        }
815
816        String::new()
817    }
818
819    fn handle_stop(&mut self) -> String {
820        self.stop_search = true;
821        self.thinking = false;
822
823        // Stop pondering if active
824        if self.pondering {
825            self.pondering = false;
826            self.ponder_move = None;
827            self.ponder_board = None;
828
829            if self.debug {
830                println!("info string Pondering stopped");
831            }
832        }
833
834        String::new()
835    }
836}
837
838/// Basic material evaluation for UCI engine (fast, no complex analysis)
839fn calculate_material_eval(board: &Board) -> f32 {
840    let mut white_material = 0.0;
841    let mut black_material = 0.0;
842    
843    // Standard piece values
844    const PAWN_VALUE: f32 = 1.0;
845    const KNIGHT_VALUE: f32 = 3.0;
846    const BISHOP_VALUE: f32 = 3.0;
847    const ROOK_VALUE: f32 = 5.0;
848    const QUEEN_VALUE: f32 = 9.0;
849    
850    for square in chess::ALL_SQUARES {
851        if let Some(piece) = board.piece_on(square) {
852            let value = match piece {
853                chess::Piece::Pawn => PAWN_VALUE,
854                chess::Piece::Knight => KNIGHT_VALUE,
855                chess::Piece::Bishop => BISHOP_VALUE,
856                chess::Piece::Rook => ROOK_VALUE,
857                chess::Piece::Queen => QUEEN_VALUE,
858                chess::Piece::King => 0.0, // King has no material value
859            };
860            
861            if board.color_on(square) == Some(chess::Color::White) {
862                white_material += value;
863            } else {
864                black_material += value;
865            }
866        }
867    }
868    
869    // Return evaluation from white's perspective
870    white_material - black_material
871}
872
873impl Default for UCIEngine {
874    fn default() -> Self {
875        Self::new()
876    }
877}
878
879/// Configuration for the UCI engine
880#[derive(Debug, Clone)]
881pub struct UCIConfig {
882    pub engine_name: String,
883    pub engine_author: String,
884    pub enable_debug: bool,
885    pub default_hash_size: i32,
886    pub default_threads: i32,
887}
888
889impl Default for UCIConfig {
890    fn default() -> Self {
891        Self {
892            engine_name: "Chess Vector Engine".to_string(),
893            engine_author: "Chess Vector Engine Team".to_string(),
894            enable_debug: false,
895            default_hash_size: 128,
896            default_threads: 1,
897        }
898    }
899}
900
901/// Run the UCI engine with default configuration
902pub fn run_uci_engine() {
903    let mut engine = UCIEngine::new();
904    engine.run();
905}
906
907/// Run the UCI engine with custom configuration
908pub fn run_uci_engine_with_config(config: UCIConfig) {
909    let mut engine = UCIEngine::new();
910    engine.engine_name = config.engine_name;
911    engine.engine_author = config.engine_author;
912    engine.debug = config.enable_debug;
913
914    // Apply configuration to options
915    if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Hash") {
916        *value = config.default_hash_size;
917    }
918    if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Threads") {
919        *value = config.default_threads;
920    }
921
922    engine.run();
923}
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928    use std::str::FromStr;
929
930    #[test]
931    fn test_uci_initialization() {
932        let engine = UCIEngine::new();
933        assert_eq!(engine.board, Board::default());
934        assert!(!engine.debug);
935        assert!(!engine.thinking);
936    }
937
938    #[test]
939    fn test_uci_command() {
940        let engine = UCIEngine::new();
941        let response = engine.handle_uci();
942        assert!(response.contains("id name"));
943        assert!(response.contains("id author"));
944        assert!(response.contains("uciok"));
945    }
946
947    #[test]
948    fn test_isready_command() {
949        let engine = UCIEngine::new();
950        let response = engine.handle_isready();
951        assert_eq!(response, "readyok");
952    }
953
954    #[test]
955    fn test_position_startpos() {
956        let mut engine = UCIEngine::new();
957        let parts = vec!["position", "startpos"];
958        engine.handle_position(&parts);
959        assert_eq!(engine.board, Board::default());
960    }
961
962    #[test]
963    fn test_position_with_moves() {
964        let mut engine = UCIEngine::new();
965        let parts = vec!["position", "startpos", "moves", "e2e4", "e7e5"];
966        engine.handle_position(&parts);
967
968        let expected_board = Board::default()
969            .make_move_new(ChessMove::from_str("e2e4").unwrap())
970            .make_move_new(ChessMove::from_str("e7e5").unwrap());
971
972        assert_eq!(engine.board, expected_board);
973    }
974
975    #[test]
976    fn test_option_setting() {
977        let mut engine = UCIEngine::new();
978        engine.set_option("Pattern_Weight", "80");
979
980        if let Some(UCIOption::Spin { value, .. }) = engine.options.get("Pattern_Weight") {
981            assert_eq!(*value, 80);
982        } else {
983            panic!("Option not found or wrong type");
984        }
985    }
986
987    #[test]
988    fn test_debug_toggle() {
989        let mut engine = UCIEngine::new();
990
991        engine.handle_debug(&["debug", "on"]);
992        assert!(engine.debug);
993
994        engine.handle_debug(&["debug", "off"]);
995        assert!(!engine.debug);
996    }
997
998    #[test]
999    fn test_pondering_option() {
1000        let engine = UCIEngine::new();
1001
1002        // Check that Ponder option exists and is enabled by default
1003        if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
1004            assert!(*value); // Should be enabled by default
1005        } else {
1006            panic!("Ponder option should be available");
1007        }
1008    }
1009
1010    #[test]
1011    fn test_ponder_command_parsing() {
1012        let mut engine = UCIEngine::new();
1013
1014        // Test "go ponder" command
1015        let response = engine.process_command("go ponder");
1016        assert_eq!(response, ""); // Should return empty string
1017
1018        // Pondering should be active (may be set asynchronously)
1019        // We don't assert on engine.pondering here due to threading
1020    }
1021
1022    #[test]
1023    fn test_ponderhit_command() {
1024        let mut engine = UCIEngine::new();
1025
1026        // Start pondering first
1027        engine.pondering = true;
1028        engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
1029
1030        // Send ponderhit
1031        let response = engine.handle_ponderhit();
1032        assert_eq!(response, "");
1033
1034        // Pondering should be stopped
1035        assert!(!engine.pondering);
1036        assert!(engine.ponder_move.is_none());
1037    }
1038
1039    #[test]
1040    fn test_stop_during_pondering() {
1041        let mut engine = UCIEngine::new();
1042
1043        // Start pondering
1044        engine.pondering = true;
1045        engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
1046
1047        // Send stop command
1048        let response = engine.handle_stop();
1049        assert_eq!(response, "");
1050
1051        // Pondering should be stopped
1052        assert!(!engine.pondering);
1053        assert!(!engine.thinking);
1054        assert!(engine.ponder_move.is_none());
1055    }
1056
1057    #[test]
1058    fn test_expected_opponent_move() {
1059        let mut engine = UCIEngine::new();
1060
1061        // Test getting expected opponent move from starting position
1062        let expected_move = engine.get_expected_opponent_move();
1063        assert!(expected_move.is_some());
1064
1065        // Should be a legal move
1066        let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&engine.board).collect();
1067        if let Some(mv) = expected_move {
1068            assert!(legal_moves.contains(&mv));
1069        }
1070    }
1071
1072    #[test]
1073    fn test_multi_pv_option() {
1074        let engine = UCIEngine::new();
1075
1076        // Check that MultiPV option exists and has correct default
1077        if let Some(UCIOption::Spin {
1078            default,
1079            min,
1080            max,
1081            value,
1082        }) = engine.options.get("MultiPV")
1083        {
1084            assert_eq!(*default, 1);
1085            assert_eq!(*min, 1);
1086            assert_eq!(*max, 10);
1087            assert_eq!(*value, 1);
1088        } else {
1089            panic!("MultiPV option should be available");
1090        }
1091    }
1092
1093    #[test]
1094    fn test_multi_pv_setting() {
1095        let mut engine = UCIEngine::new();
1096
1097        // Test setting MultiPV option
1098        let response = engine.process_command("setoption name MultiPV value 3");
1099        assert_eq!(response, "");
1100
1101        // Check that option was set
1102        if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1103            assert_eq!(*value, 3);
1104        } else {
1105            panic!("MultiPV option should exist");
1106        }
1107    }
1108
1109    #[test]
1110    fn test_single_pv_vs_multi_pv() {
1111        let mut engine = UCIEngine::new();
1112
1113        // Test that engine accepts both single and multi-PV modes
1114        // (We can't easily test the actual output format in unit tests due to threading)
1115
1116        // Set to single PV
1117        engine.process_command("setoption name MultiPV value 1");
1118        if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1119            assert_eq!(*value, 1);
1120        }
1121
1122        // Set to multi-PV
1123        engine.process_command("setoption name MultiPV value 5");
1124        if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
1125            assert_eq!(*value, 5);
1126        }
1127    }
1128}