aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam <adammegarules1@gmail.com>2026-08-24 10:30:53 +0200
committerAdam <adammegarules1@gmail.com>2026-08-24 10:30:53 +0200
commit22718aea57fde4e325aea4c0d36d76b0d1609fa7 (patch)
treee01eb8fc249601b95a3ee7da601c62d271d43e5c
parent9a9cbe07f062ce7734ae2b7c6161c0e8ec4b0e4c (diff)
refactor(tests): improving tests by creating testing cli app
-rw-r--r--cli/.gitignore1
-rw-r--r--cli/Cargo.lock107
-rw-r--r--cli/Cargo.toml8
-rw-r--r--cli/src/main.rs233
-rwxr-xr-xscripts/perftbin0 -> 631864 bytes
-rwxr-xr-xscripts/tests/perft.bash61
-rw-r--r--src/main.cpp42
-rw-r--r--src/uci.cpp31
8 files changed, 380 insertions, 103 deletions
diff --git a/cli/.gitignore b/cli/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/cli/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/cli/Cargo.lock b/cli/Cargo.lock
new file mode 100644
index 0000000..6341cb6
--- /dev/null
+++ b/cli/Cargo.lock
@@ -0,0 +1,107 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "cli"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
new file mode 100644
index 0000000..9694539
--- /dev/null
+++ b/cli/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "cli"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
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);
+ }
+}
diff --git a/scripts/perft b/scripts/perft
new file mode 100755
index 0000000..1319086
--- /dev/null
+++ b/scripts/perft
Binary files differ
diff --git a/scripts/tests/perft.bash b/scripts/tests/perft.bash
deleted file mode 100755
index 335f7de..0000000
--- a/scripts/tests/perft.bash
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env bash
-
-status=0
-
-testPerft() {
- local fen="$1"
- local depth="$2"
- local expected="$3"
-
- echo ""
-
- result=$(./build/mono "perft" $depth "$fen")
- count=$(echo "$result" | jq -r '.result')
- took=$(echo "$result" | jq -r '.ms')
- if [ "$count" = "$expected" ]; then
- echo "Results match"
- echo "Took: $took ms"
- else
- echo "RESULTS DONT MATCH"
- echo "Took: $took ms"
- echo "Depth: $depth"
- echo "Fen: $fen"
- echo "Expected: $expected"
- echo "Got: $count"
- status=1
- fi
-}
-
-configs=(
- # starting pos
- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1|1|20"
- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1|2|400"
- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1|3|8902"
- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1|4|197281"
- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1|5|4865609"
-
- # kiwipete
- "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -|1|48"
- "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -|2|2039"
- "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -|3|97862"
- "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -|4|4085603"
-
- # even more cool ones
- "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1|1|14"
- "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1|2|191"
- "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1|3|2812"
- "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1|4|43238"
-
- "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1|1|6"
- "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1|2|264"
- "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1|3|9467"
- "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1|4|422333"
-)
-
-for config in "${configs[@]}"; do
- IFS='|' read -r fen depth expected <<< "$config"
- testPerft "$fen" "$depth" "$expected"
- if [ $status -ne 0 ]; then
- exit "$status"
- fi
-done
diff --git a/src/main.cpp b/src/main.cpp
index 27b428d..e7f2d87 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -7,48 +7,11 @@
#include <string>
#include "board/board.hpp"
-#include "board/fen.hpp"
#include "hash.hpp"
#include "misc.hpp"
#include "moves.hpp"
#include "uci.hpp"
-static void perft(int argc, char *argv[]) {
- if (argc < 4 || std::string(argv[1]) != "perft") {
- std::cout << "Usage: ./mono\n";
- std::cout << " ./mono perft <non-negative depth> <fen>\n";
- exit(1);
- }
-
- uint32_t depth = 0;
- try {
- depth = static_cast<uint32_t>(std::stoi(argv[2]));
- } catch (const std::exception &) {
- std::cout << "info string usage: ./mono perft <non-negative depth>\n";
- exit(1);
- }
-
- std::string fen = argv[3];
-
- auto start = std::chrono::steady_clock::now();
-
- Game game = initBoard(fen, nullptr);
- uint64_t count = 0;
-#ifdef PERFT_DIVIDE
- count = MoveGenTestDivide(depth, &game);
-#else
- count = MoveGenTest(depth, &game);
-#endif
-
- auto end = std::chrono::steady_clock::now();
- auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
- .count();
- std::ostringstream oss;
- oss << "{\"ms\": " << ms << ", \"result\": " << count << "}";
-
- std::cout << oss.str() << "\n";
-}
-
static void hashCmd(int argc, char *argv[]) {
if (argc < 3) {
std::cout << "Usage: ./mono hash <fen> [moves...]\n";
@@ -79,11 +42,6 @@ int main(int argc, char *argv[]) {
InitHashing();
initMagicBitboards();
- if (argc >= 2 && std::string(argv[1]) == "perft") {
- perft(argc, argv);
- return 0;
- }
-
if (argc >= 2 && std::string(argv[1]) == "hash") {
hashCmd(argc, argv);
return 0;
diff --git a/src/uci.cpp b/src/uci.cpp
index 7cff997..5f62169 100644
--- a/src/uci.cpp
+++ b/src/uci.cpp
@@ -1,6 +1,7 @@
#include <array>
#include <cassert>
#include <cctype>
+#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
@@ -351,5 +352,35 @@ void Uci() {
uint64_t hash = GenerateHashFromScratch(&game);
std::cout << hash << "\n";
}
+ if (cmd.starts_with("perft")) {
+
+ uint32_t depth = 0;
+
+ std::stringstream ss(cmd);
+ std::string token;
+
+ ss >> token; // position
+
+ ss >> depth;
+
+ auto start = std::chrono::steady_clock::now();
+
+ uint64_t count = 0;
+
+#ifdef PERFT_DIVIDE
+ count = MoveGenTestDivide(depth, &game);
+#else
+ count = MoveGenTest(depth, &game);
+#endif
+
+ auto end = std::chrono::steady_clock::now();
+ auto ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
+ .count();
+ std::ostringstream oss;
+ oss << "{\"ms\": " << ms << ", \"result\": " << count << "}";
+
+ std::cout << oss.str() << "\n";
+ }
}
}