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 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, }, ]; 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 line = engine.read_line(); let result = parse_perft_result(&line); 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 parse_perft_result(raw: &str) -> Result { serde_json::from_str(raw) } 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 PERFT_TESTS.iter() { test_perft(test, &mut engine); } }