diff options
| author | Adam <adammegarules1@gmail.com> | 2026-08-24 10:30:53 +0200 |
|---|---|---|
| committer | Adam <adammegarules1@gmail.com> | 2026-08-24 10:30:53 +0200 |
| commit | 22718aea57fde4e325aea4c0d36d76b0d1609fa7 (patch) | |
| tree | e01eb8fc249601b95a3ee7da601c62d271d43e5c | |
| parent | 9a9cbe07f062ce7734ae2b7c6161c0e8ec4b0e4c (diff) | |
refactor(tests): improving tests by creating testing cli app
| -rw-r--r-- | cli/.gitignore | 1 | ||||
| -rw-r--r-- | cli/Cargo.lock | 107 | ||||
| -rw-r--r-- | cli/Cargo.toml | 8 | ||||
| -rw-r--r-- | cli/src/main.rs | 233 | ||||
| -rwxr-xr-x | scripts/perft | bin | 0 -> 631864 bytes | |||
| -rwxr-xr-x | scripts/tests/perft.bash | 61 | ||||
| -rw-r--r-- | src/main.cpp | 42 | ||||
| -rw-r--r-- | src/uci.cpp | 31 |
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 Binary files differnew file mode 100755 index 0000000..1319086 --- /dev/null +++ b/scripts/perft 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"; + } } } |
