aboutsummaryrefslogtreecommitdiff
path: root/cli/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/src/main.rs')
-rw-r--r--cli/src/main.rs233
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);
+ }
+}