diff options
Diffstat (limited to 'cli/src/main.rs')
| -rw-r--r-- | cli/src/main.rs | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..963544c --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,233 @@ +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<ChildStdout>, +} + +#[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<String> { + 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<PerftResult> { + serde_json::from_str(raw) +} + +fn main() { + let args: Vec<String> = env::args().collect(); + + if args.len() != 2 { + println!("usage: ./cli <path to test executable>"); + 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); + } +} |
