use chess::{Board, ChessMove, Color};
use std::collections::HashMap;
use std::io::{self, BufRead, Write};
use std::str::FromStr;
use std::thread;
use std::time::{Duration, Instant};
use crate::{ChessVectorEngine, HybridConfig, TacticalConfig};
pub struct UCIEngine {
engine: ChessVectorEngine,
board: Board,
debug: bool,
engine_name: String,
engine_author: String,
options: HashMap<String, UCIOption>,
thinking: bool,
stop_search: bool,
pondering: bool,
ponder_move: Option<ChessMove>,
ponder_board: Option<Board>,
}
#[derive(Debug, Clone)]
pub enum UCIOption {
Check {
default: bool,
value: bool,
},
Spin {
default: i32,
min: i32,
max: i32,
value: i32,
},
Combo {
default: String,
options: Vec<String>,
value: String,
},
Button,
String {
default: String,
value: String,
},
}
#[derive(Debug, Clone)]
pub struct SearchInfo {
depth: u32,
#[allow(dead_code)]
seldepth: Option<u32>,
time: u64, nodes: u64,
nps: u64, #[allow(dead_code)]
score: SearchScore,
#[allow(dead_code)]
pv: Vec<ChessMove>, #[allow(dead_code)]
currmove: Option<ChessMove>,
#[allow(dead_code)]
currmovenumber: Option<u32>,
}
#[derive(Debug, Clone)]
pub enum SearchScore {
Centipawns(i32),
Mate(i32), }
impl UCIEngine {
pub fn new() -> Self {
let mut engine = ChessVectorEngine::new_lightweight(1024);
engine.enable_opening_book();
engine.enable_tactical_search_default();
engine.configure_hybrid_evaluation(HybridConfig::default());
let _ = engine.enable_strategic_motifs();
let mut options = HashMap::new();
options.insert(
"Hash".to_string(),
UCIOption::Spin {
default: 128,
min: 1,
max: 2048,
value: 128,
},
);
options.insert(
"Threads".to_string(),
UCIOption::Spin {
default: 1,
min: 1,
max: 64,
value: 1,
},
);
options.insert(
"MultiPV".to_string(),
UCIOption::Spin {
default: 1,
min: 1,
max: 10,
value: 1,
},
);
options.insert(
"Pattern_Weight".to_string(),
UCIOption::Spin {
default: 60,
min: 0,
max: 100,
value: 60,
},
);
options.insert(
"Tactical_Depth".to_string(),
UCIOption::Spin {
default: 3,
min: 1,
max: 10,
value: 3,
},
);
options.insert(
"Pattern_Confidence_Threshold".to_string(),
UCIOption::Spin {
default: 75,
min: 0,
max: 100,
value: 75,
},
);
options.insert(
"Enable_LSH".to_string(),
UCIOption::Check {
default: true,
value: true,
},
);
options.insert(
"Enable_GPU".to_string(),
UCIOption::Check {
default: true,
value: true,
},
);
options.insert(
"Ponder".to_string(),
UCIOption::Check {
default: true,
value: true,
},
);
options.insert("Load_Position_Data".to_string(), UCIOption::Button);
Self {
engine,
board: Board::default(),
debug: false,
engine_name: "Chess Vector Engine".to_string(),
engine_author: "Chess Vector Engine Team".to_string(),
options,
thinking: false,
stop_search: false,
pondering: false,
ponder_move: None,
ponder_board: None,
}
}
pub fn run(&mut self) {
let stdin = io::stdin();
let mut stdout = io::stdout();
for line in stdin.lock().lines() {
match line {
Ok(command) => {
let response = self.process_command(command.trim());
if !response.is_empty() {
let _ = writeln!(stdout, "{response}");
let _ = stdout.flush();
}
if command.trim() == "quit" {
break;
}
}
Err(e) => {
if self.debug {
let _ = writeln!(stdout, "Error reading input: {e}");
}
break;
}
}
}
}
fn process_command(&mut self, command: &str) -> String {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.is_empty() {
return String::new();
}
match parts[0] {
"uci" => self.handle_uci(),
"debug" => self.handle_debug(&parts),
"isready" => self.handle_isready(),
"setoption" => self.handle_setoption(&parts),
"register" => String::new(), "ucinewgame" => self.handle_ucinewgame(),
"position" => self.handle_position(&parts),
"go" => self.handle_go(&parts),
"stop" => self.handle_stop(),
"ponderhit" => self.handle_ponderhit(),
"quit" => String::new(),
_ => {
if self.debug {
format!("Unknown command: {command}")
} else {
String::new()
}
}
}
}
fn handle_uci(&self) -> String {
let mut response = String::new();
response.push_str(&format!("id name {}\n", self.engine_name));
response.push_str(&format!("id author {}\n", self.engine_author));
for (name, option) in &self.options {
match option {
UCIOption::Check { default, .. } => {
response.push_str(&format!(
"option name {name} type check default {default}\n"
));
}
UCIOption::Spin {
default, min, max, ..
} => {
response.push_str(&format!(
"option name {name} type spin default {default} min {min} max {max}\n"
));
}
UCIOption::Combo {
default, options, ..
} => {
let combo_options = options.join(" var ");
response.push_str(&format!(
"option name {name} type combo default {default} var {combo_options}\n"
));
}
UCIOption::Button => {
response.push_str(&format!("option name {name} type button\n"));
}
UCIOption::String { default, .. } => {
response.push_str(&format!(
"option name {name} type string default {default}\n"
));
}
}
}
response.push_str("uciok");
response
}
fn handle_debug(&mut self, parts: &[&str]) -> String {
if parts.len() >= 2 {
match parts[1] {
"on" => self.debug = true,
"off" => self.debug = false,
_ => {}
}
}
String::new()
}
fn handle_isready(&self) -> String {
"readyok".to_string()
}
fn handle_setoption(&mut self, parts: &[&str]) -> String {
if parts.len() >= 4 && parts[1] == "name" {
let mut name_parts = Vec::new();
let mut value_parts = Vec::new();
let mut in_value = false;
for &part in &parts[2..] {
if part == "value" {
in_value = true;
} else if in_value {
value_parts.push(part);
} else {
name_parts.push(part);
}
}
let name = name_parts.join(" ");
let value = value_parts.join(" ");
self.set_option(&name, &value);
}
String::new()
}
fn set_option(&mut self, name: &str, value: &str) {
if let Some(option) = self.options.get_mut(name) {
match option {
UCIOption::Check {
value: ref mut val, ..
} => {
*val = value == "true";
}
UCIOption::Spin {
value: ref mut val,
min,
max,
..
} => {
if let Ok(new_val) = value.parse::<i32>() {
if new_val >= *min && new_val <= *max {
*val = new_val;
}
}
}
UCIOption::Combo {
value: ref mut val,
options,
..
} => {
if options.contains(&value.to_string()) {
*val = value.to_string();
}
}
UCIOption::String {
value: ref mut val, ..
} => {
*val = value.to_string();
}
UCIOption::Button => {
if name == "Load_Position_Data" {
let _ = self.engine.auto_load_training_data();
}
}
}
}
self.apply_options();
}
fn apply_options(&mut self) {
if let Some(UCIOption::Spin {
value: pattern_weight,
..
}) = self.options.get("Pattern_Weight")
{
let weight = (*pattern_weight as f32) / 100.0;
let config = HybridConfig {
pattern_weight: weight,
..HybridConfig::default()
};
self.engine.configure_hybrid_evaluation(config);
}
if let Some(UCIOption::Spin { value: depth, .. }) = self.options.get("Tactical_Depth") {
let config = TacticalConfig {
max_depth: *depth as u32,
..TacticalConfig::default()
};
self.engine.enable_tactical_search(config);
}
if let Some(UCIOption::Spin {
value: threshold, ..
}) = self.options.get("Pattern_Confidence_Threshold")
{
let config = HybridConfig {
pattern_confidence_threshold: (*threshold as f32) / 100.0,
..HybridConfig::default()
};
self.engine.configure_hybrid_evaluation(config);
}
if let Some(UCIOption::Check {
value: enable_lsh, ..
}) = self.options.get("Enable_LSH")
{
if *enable_lsh && !self.engine.is_lsh_enabled() {
self.engine.enable_lsh(8, 16);
}
}
}
fn handle_ucinewgame(&mut self) -> String {
self.board = Board::default();
String::new()
}
fn handle_position(&mut self, parts: &[&str]) -> String {
if parts.len() < 2 {
return String::new();
}
let mut board = if parts[1] == "startpos" {
Board::default()
} else if parts[1] == "fen" && parts.len() >= 8 {
let fen = parts[2..8].join(" ");
match Board::from_str(&fen) {
Ok(board) => board,
Err(_) => {
if self.debug {
return "info string Invalid FEN".to_string();
}
return String::new();
}
}
} else {
return String::new();
};
if let Some(moves_idx) = parts.iter().position(|&x| x == "moves") {
for move_str in &parts[moves_idx + 1..] {
if let Ok(chess_move) = ChessMove::from_str(move_str) {
if board.legal(chess_move) {
board = board.make_move_new(chess_move);
} else if self.debug {
return format!("info string Illegal move: {move_str}");
}
}
}
}
self.board = board;
String::new()
}
fn handle_go(&mut self, parts: &[&str]) -> String {
if self.thinking {
return String::new();
}
let mut wtime = None;
let mut btime = None;
let mut _winc: Option<u64> = None;
let mut _binc: Option<u64> = None;
let mut movestogo = None;
let mut depth = None;
let mut _nodes: Option<u64> = None;
let mut movetime = None;
let mut infinite = false;
let mut ponder = false;
let mut i = 1;
while i < parts.len() {
match parts[i] {
"wtime" if i + 1 < parts.len() => {
wtime = parts[i + 1].parse().ok();
i += 2;
}
"btime" if i + 1 < parts.len() => {
btime = parts[i + 1].parse().ok();
i += 2;
}
"winc" if i + 1 < parts.len() => {
_winc = parts[i + 1].parse().ok();
i += 2;
}
"binc" if i + 1 < parts.len() => {
_binc = parts[i + 1].parse().ok();
i += 2;
}
"movestogo" if i + 1 < parts.len() => {
movestogo = parts[i + 1].parse().ok();
i += 2;
}
"depth" if i + 1 < parts.len() => {
depth = parts[i + 1].parse().ok();
i += 2;
}
"nodes" if i + 1 < parts.len() => {
_nodes = parts[i + 1].parse().ok();
i += 2;
}
"movetime" if i + 1 < parts.len() => {
movetime = parts[i + 1].parse().ok();
i += 2;
}
"infinite" => {
infinite = true;
i += 1;
}
"ponder" => {
ponder = true;
i += 1;
}
_ => i += 1,
}
}
let search_time = if let Some(mt) = movetime {
Duration::from_millis(mt)
} else if infinite {
Duration::from_secs(3600) } else {
let our_time = if self.board.side_to_move() == Color::White {
wtime.unwrap_or(30000)
} else {
btime.unwrap_or(30000)
};
let moves_left = movestogo.unwrap_or(30);
let time_per_move = our_time / moves_left.max(1);
Duration::from_millis(time_per_move.min(our_time / 2))
};
if ponder {
self.start_ponder_search(search_time, depth);
} else {
self.start_search(search_time, depth);
}
String::new()
}
fn start_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
self.thinking = true;
self.stop_search = false;
let board = self.board;
let mut engine = self.engine.clone(); let start_time = Instant::now();
let multi_pv = if let Some(UCIOption::Spin { value, .. }) = self.options.get("MultiPV") {
*value as usize
} else {
1
};
thread::spawn(move || {
let mut search_info = SearchInfo {
depth: 1,
seldepth: None,
time: 0,
nodes: 0,
nps: 0,
score: SearchScore::Centipawns(0),
pv: Vec::new(),
currmove: None,
currmovenumber: None,
};
let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&board).collect();
if legal_moves.is_empty() {
println!("bestmove 0000"); return;
}
let mut move_evaluations: Vec<(ChessMove, f32)> = Vec::new();
let mut nodes_searched = 0;
use crate::evaluation_calibration::CalibratedEvaluator;
let calibrated_evaluator = CalibratedEvaluator::new(
crate::evaluation_calibration::CalibrationConfig::default()
);
for chess_move in &legal_moves {
let temp_board = board.make_move_new(*chess_move);
nodes_searched += 1;
let position_eval = if let Some(entry) = engine.get_opening_entry(&temp_board) {
entry.evaluation
} else {
calibrated_evaluator.evaluate_centipawns(&temp_board) as f32 / 100.0
};
{
let eval_for_us = if board.side_to_move() == chess::Color::White {
position_eval
} else {
-position_eval
};
move_evaluations.push((*chess_move, eval_for_us));
}
}
move_evaluations
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let (best_move, _) = if !move_evaluations.is_empty() {
move_evaluations[0]
} else {
(legal_moves[0], 0.0)
};
search_info.time = start_time.elapsed().as_millis() as u64;
search_info.nodes = nodes_searched;
search_info.nps = if search_info.time > 0 {
(search_info.nodes * 1000) / search_info.time
} else {
0
};
let pv_count = move_evaluations.len().min(multi_pv);
for (pv_index, (chess_move, eval)) in move_evaluations.iter().take(pv_count).enumerate()
{
let score_cp = (*eval * 100.0) as i32;
if multi_pv == 1 {
println!(
"info depth {} score cp {} time {} nodes {} nps {} pv {}",
search_info.depth,
score_cp,
search_info.time,
search_info.nodes,
search_info.nps,
chess_move
);
} else {
println!(
"info depth {} multipv {} score cp {} time {} nodes {} nps {} pv {}",
search_info.depth,
pv_index + 1,
score_cp,
search_info.time,
search_info.nodes,
search_info.nps,
chess_move
);
}
}
println!("bestmove {best_move}");
});
}
fn start_ponder_search(&mut self, _max_time: Duration, _max_depth: Option<u32>) {
if let Some(UCIOption::Check { value, .. }) = self.options.get("Ponder") {
if !value {
return; }
} else {
return; }
self.pondering = true;
self.thinking = true;
self.stop_search = false;
if let Some(ponder_move) = self.get_expected_opponent_move() {
self.ponder_move = Some(ponder_move);
self.ponder_board = Some(self.board.make_move_new(ponder_move));
if self.debug {
println!("info string Pondering on {ponder_move}");
}
let ponder_board = self.ponder_board.unwrap();
let mut engine = self.engine.clone();
let debug = self.debug;
thread::spawn(move || {
if let Some(eval) = engine.evaluate_position(&ponder_board) {
if debug {
println!("info string Ponder evaluation: {eval:.2}");
}
}
});
}
}
fn get_expected_opponent_move(&mut self) -> Option<ChessMove> {
use chess::MoveGen;
let legal_moves: Vec<ChessMove> = MoveGen::new_legal(&self.board).collect();
if !legal_moves.is_empty() {
let mut best_move = legal_moves[0];
let mut best_eval = f32::NEG_INFINITY;
for chess_move in legal_moves.iter().take(5) {
let new_board = self.board.make_move_new(*chess_move);
if let Some(eval) = self.engine.evaluate_position(&new_board) {
let opponent_eval = -eval;
if opponent_eval > best_eval {
best_eval = opponent_eval;
best_move = *chess_move;
}
}
}
Some(best_move)
} else {
None
}
}
fn handle_ponderhit(&mut self) -> String {
if self.pondering {
self.pondering = false;
if self.debug {
println!("info string Ponderhit received - converting ponder to normal search");
}
if let Some(ponder_board) = self.ponder_board {
let legal_moves: Vec<ChessMove> =
chess::MoveGen::new_legal(&ponder_board).collect();
if !legal_moves.is_empty() {
let mut best_move = legal_moves[0];
let mut best_eval = f32::NEG_INFINITY;
for chess_move in legal_moves.iter().take(5) {
let new_board = ponder_board.make_move_new(*chess_move);
if let Some(eval) = self.engine.evaluate_position(&new_board) {
if eval > best_eval {
best_eval = eval;
best_move = *chess_move;
}
}
}
thread::spawn(move || {
println!("bestmove {best_move}");
});
}
}
self.ponder_move = None;
self.ponder_board = None;
}
String::new()
}
fn handle_stop(&mut self) -> String {
self.stop_search = true;
self.thinking = false;
if self.pondering {
self.pondering = false;
self.ponder_move = None;
self.ponder_board = None;
if self.debug {
println!("info string Pondering stopped");
}
}
String::new()
}
}
fn calculate_material_eval(board: &Board) -> f32 {
let mut white_material = 0.0;
let mut black_material = 0.0;
const PAWN_VALUE: f32 = 1.0;
const KNIGHT_VALUE: f32 = 3.0;
const BISHOP_VALUE: f32 = 3.0;
const ROOK_VALUE: f32 = 5.0;
const QUEEN_VALUE: f32 = 9.0;
for square in chess::ALL_SQUARES {
if let Some(piece) = board.piece_on(square) {
let value = match piece {
chess::Piece::Pawn => PAWN_VALUE,
chess::Piece::Knight => KNIGHT_VALUE,
chess::Piece::Bishop => BISHOP_VALUE,
chess::Piece::Rook => ROOK_VALUE,
chess::Piece::Queen => QUEEN_VALUE,
chess::Piece::King => 0.0, };
if board.color_on(square) == Some(chess::Color::White) {
white_material += value;
} else {
black_material += value;
}
}
}
white_material - black_material
}
impl Default for UCIEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct UCIConfig {
pub engine_name: String,
pub engine_author: String,
pub enable_debug: bool,
pub default_hash_size: i32,
pub default_threads: i32,
}
impl Default for UCIConfig {
fn default() -> Self {
Self {
engine_name: "Chess Vector Engine".to_string(),
engine_author: "Chess Vector Engine Team".to_string(),
enable_debug: false,
default_hash_size: 128,
default_threads: 1,
}
}
}
pub fn run_uci_engine() {
let mut engine = UCIEngine::new();
engine.run();
}
pub fn run_uci_engine_with_config(config: UCIConfig) {
let mut engine = UCIEngine::new();
engine.engine_name = config.engine_name;
engine.engine_author = config.engine_author;
engine.debug = config.enable_debug;
if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Hash") {
*value = config.default_hash_size;
}
if let Some(UCIOption::Spin { value, .. }) = engine.options.get_mut("Threads") {
*value = config.default_threads;
}
engine.run();
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_uci_initialization() {
let engine = UCIEngine::new();
assert_eq!(engine.board, Board::default());
assert!(!engine.debug);
assert!(!engine.thinking);
}
#[test]
fn test_uci_command() {
let engine = UCIEngine::new();
let response = engine.handle_uci();
assert!(response.contains("id name"));
assert!(response.contains("id author"));
assert!(response.contains("uciok"));
}
#[test]
fn test_isready_command() {
let engine = UCIEngine::new();
let response = engine.handle_isready();
assert_eq!(response, "readyok");
}
#[test]
fn test_position_startpos() {
let mut engine = UCIEngine::new();
let parts = vec!["position", "startpos"];
engine.handle_position(&parts);
assert_eq!(engine.board, Board::default());
}
#[test]
fn test_position_with_moves() {
let mut engine = UCIEngine::new();
let parts = vec!["position", "startpos", "moves", "e2e4", "e7e5"];
engine.handle_position(&parts);
let expected_board = Board::default()
.make_move_new(ChessMove::from_str("e2e4").unwrap())
.make_move_new(ChessMove::from_str("e7e5").unwrap());
assert_eq!(engine.board, expected_board);
}
#[test]
fn test_option_setting() {
let mut engine = UCIEngine::new();
engine.set_option("Pattern_Weight", "80");
if let Some(UCIOption::Spin { value, .. }) = engine.options.get("Pattern_Weight") {
assert_eq!(*value, 80);
} else {
panic!("Option not found or wrong type");
}
}
#[test]
fn test_debug_toggle() {
let mut engine = UCIEngine::new();
engine.handle_debug(&["debug", "on"]);
assert!(engine.debug);
engine.handle_debug(&["debug", "off"]);
assert!(!engine.debug);
}
#[test]
fn test_pondering_option() {
let engine = UCIEngine::new();
if let Some(UCIOption::Check { value, .. }) = engine.options.get("Ponder") {
assert!(*value); } else {
panic!("Ponder option should be available");
}
}
#[test]
fn test_ponder_command_parsing() {
let mut engine = UCIEngine::new();
let response = engine.process_command("go ponder");
assert_eq!(response, "");
}
#[test]
fn test_ponderhit_command() {
let mut engine = UCIEngine::new();
engine.pondering = true;
engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
let response = engine.handle_ponderhit();
assert_eq!(response, "");
assert!(!engine.pondering);
assert!(engine.ponder_move.is_none());
}
#[test]
fn test_stop_during_pondering() {
let mut engine = UCIEngine::new();
engine.pondering = true;
engine.ponder_move = Some(ChessMove::from_str("e2e4").unwrap());
let response = engine.handle_stop();
assert_eq!(response, "");
assert!(!engine.pondering);
assert!(!engine.thinking);
assert!(engine.ponder_move.is_none());
}
#[test]
fn test_expected_opponent_move() {
let mut engine = UCIEngine::new();
let expected_move = engine.get_expected_opponent_move();
assert!(expected_move.is_some());
let legal_moves: Vec<ChessMove> = chess::MoveGen::new_legal(&engine.board).collect();
if let Some(mv) = expected_move {
assert!(legal_moves.contains(&mv));
}
}
#[test]
fn test_multi_pv_option() {
let engine = UCIEngine::new();
if let Some(UCIOption::Spin {
default,
min,
max,
value,
}) = engine.options.get("MultiPV")
{
assert_eq!(*default, 1);
assert_eq!(*min, 1);
assert_eq!(*max, 10);
assert_eq!(*value, 1);
} else {
panic!("MultiPV option should be available");
}
}
#[test]
fn test_multi_pv_setting() {
let mut engine = UCIEngine::new();
let response = engine.process_command("setoption name MultiPV value 3");
assert_eq!(response, "");
if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
assert_eq!(*value, 3);
} else {
panic!("MultiPV option should exist");
}
}
#[test]
fn test_single_pv_vs_multi_pv() {
let mut engine = UCIEngine::new();
engine.process_command("setoption name MultiPV value 1");
if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
assert_eq!(*value, 1);
}
engine.process_command("setoption name MultiPV value 5");
if let Some(UCIOption::Spin { value, .. }) = engine.options.get("MultiPV") {
assert_eq!(*value, 5);
}
}
}