use serde::Deserialize; use serde_json::Result; use std::env; use std::io::{BufRead, BufReader, Write}; use std::process::{ChildStdin, ChildStdout, Command, Stdio}; use std::string::String; pub struct ChessEngine { stdin: ChildStdin, reader: BufReader, } #[derive(Deserialize)] struct PerftResult { ms: i32, result: u64, } #[derive(Debug)] pub struct PerftTest { pub position: &'static str, pub depth: i32, pub expected: u64, } pub struct HashTest { pub position: &'static str, pub moves: &'static str, pub hash: u64, } pub const PERFT_TESTS: &[PerftTest; 17] = &[ PerftTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", depth: 1, expected: 20, }, PerftTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", depth: 2, expected: 400, }, PerftTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", depth: 3, expected: 8902, }, PerftTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", depth: 4, expected: 197281, }, PerftTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", depth: 5, expected: 4865609, }, // kiwipete PerftTest { position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", depth: 1, expected: 48, }, PerftTest { position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", depth: 2, expected: 2039, }, PerftTest { position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", depth: 3, expected: 97862, }, PerftTest { position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", depth: 4, expected: 4085603, }, // other ones PerftTest { position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", depth: 1, expected: 14, }, PerftTest { position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", depth: 2, expected: 191, }, PerftTest { position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", depth: 3, expected: 2812, }, PerftTest { position: "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", depth: 4, expected: 43238, }, // other position PerftTest { position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", depth: 1, expected: 6, }, PerftTest { position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", depth: 2, expected: 264, }, PerftTest { position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", depth: 3, expected: 9467, }, PerftTest { position: "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", depth: 4, expected: 422333, }, ]; pub const HASH_TESTS: &[HashTest; 12] = &[ HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "", hash: 5060803636482931868, }, // one moves HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "d2d4", hash: 9443689642921087454, }, HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "e2e4", hash: 9384546495678726550, }, HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "c2c4", hash: 14562399549841627035, }, // two moves HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "e2e4 e7e5", hash: 595762792459712928, }, HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "e2e4 c7c5", hash: 7227515431820872427, }, HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "e2e4 e7e6", hash: 17603279437685838276, }, HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "e2e4 c7c6", hash: 1172755318016414253, }, HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "d2d4 d7d5", hash: 460664201775194104, }, // three moves HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "e2e4 e7e5 g1f3", hash: 15213300192948443293, }, HashTest { position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", moves: "d2d4 d7d5 c2c4", hash: 9963937660605248767, }, // kiwipete HashTest { position: "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", moves: "", hash: 14109232545397825053, }, ]; impl ChessEngine { /// Spawns the engine process and initializes UCI mode pub fn new(path_to_engine: &str) -> Self { let mut child = Command::new(path_to_engine) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Failed to start chess engine"); let stdin = child.stdin.take().expect("Failed to open stdin"); let stdout = child.stdout.take().expect("Failed to open stdout"); let reader = BufReader::new(stdout); let mut engine = Self { stdin, reader }; // Boot up UCI mode engine.send_command("uci"); engine.wait_for_response("uciok"); engine } /// Sends a clean string command to the engine. /// Explicitly appends a newline and flushes the stream. pub fn send_command(&mut self, cmd: &str) { let formatted = format!("{}\n", cmd); self.stdin .write_all(formatted.as_bytes()) .expect("Failed to write to engine"); self.stdin.flush().expect("Failed to flush stdin"); } /// Blocks and reads lines until a specific keyword signature is found pub fn wait_for_response(&mut self, expected_keyword: &str) -> Vec { let mut lines = Vec::new(); let mut line = String::new(); while self.reader.read_line(&mut line).is_ok() { let clean_line = line.trim().to_string(); if clean_line.starts_with("info") { println!("{}", clean_line) } lines.push(clean_line.clone()); // Clear buffer for the next read loop line.clear(); if clean_line.contains(expected_keyword) { break; } } lines } pub fn read_line(&mut self) -> String { let mut line = String::new(); let _ = self.reader.read_line(&mut line); let clean_line = line.trim().to_string(); if clean_line.starts_with("info") { println!("{}", clean_line) } clean_line } } fn test_perft(test: &PerftTest, engine: &mut ChessEngine) { let cmd = "position fen ".to_string() + &test.position; engine.send_command(&cmd); let cmd = "perft ".to_string() + &test.depth.to_string(); engine.send_command(&cmd); let raw = engine.read_line(); let result: Result = serde_json::from_str(&raw); match result { Err(err) => { println!("error: {}", err); } Ok(perft) => { if perft.result == test.expected { println!( "passed (Depth {}): {} nodes in {}ms", test.depth, perft.result, perft.ms ); } else { println!( "FAILED Depth: {}, Expected: {}, got: {}, position: {}", test.depth, test.expected, perft.result, test.position, ); std::process::exit(1); } } } } fn test_hash(test: &HashTest, engine: &mut ChessEngine) { let cmd = "position fen ".to_string() + &test.position + " moves " + &test.moves; engine.send_command(&cmd); engine.send_command("hash"); let line = engine.read_line(); let hash: u64 = match line.trim().parse() { Ok(num) => num, Err(_) => { println!("Failed to parse hash: {}", line); std::process::exit(1) } }; if hash == test.hash { println!( "passed {}: position: {}, moves: {}", hash, test.position, test.moves ); } else { println!("FAILED position: {}, moves: {}", test.position, test.moves); std::process::exit(1); } } fn main() { let args: Vec = env::args().collect(); if args.len() != 2 { println!("usage: ./cli "); return; } let engine_path = &args[1]; let mut engine = ChessEngine::new(engine_path); println!("Engine running"); engine.send_command("isready"); engine.wait_for_response("readyok"); for test in HASH_TESTS.iter() { test_hash(&test, &mut engine); } for test in PERFT_TESTS.iter() { test_perft(test, &mut engine); } }