diff options
| -rw-r--r-- | PROJECT.md | 86 | ||||
| -rw-r--r-- | STYLE_GUIDE.md | 123 | ||||
| -rw-r--r-- | src/engine/thread.c | 11 | ||||
| -rw-r--r-- | src/uci.c | 22 | ||||
| -rw-r--r-- | src/uci/response.c | 659 | ||||
| -rw-r--r-- | src/uci/state.c | 156 | ||||
| -rw-r--r-- | tests/generated.c | 94 |
7 files changed, 1072 insertions, 79 deletions
@@ -23,7 +23,9 @@ gacrux/ │ ├── engine/ # Engine internals headers │ │ ├── magic.h # Magic-bitboard attack tables │ │ ├── moves.h # move_t, moves_t, move generation API -│ │ ├── threads.h # Multi-threaded search thread structs & API +│ │ ├── search.h # Alpha-beta search, qsearch, PV reconstruction +│ │ ├── evaluate.h # Static evaluation (material + PST) +│ │ ├── threads.h # Multi-threaded DTS search thread structs & API │ │ └── ttable.h # Transposition table (Zobrist hashing, buckets) │ ├── ipc/ # Inter-process / thread IPC headers │ │ ├── state.h # ipc_state_t: shared atomic event state @@ -57,8 +59,10 @@ gacrux/ │ │ └── moves.c # ipc_moves_t add/init/deinit │ └── engine/ │ ├── moves.c # Legal move generation (pseudolegal + legality filter) -│ ├── threads.c # pthread creation/cancel/cleanup for DTS search -│ ├── thread.c # child_thread() & root_thread() entry points +│ ├── search.c # Alpha-beta search, qsearch, PV reconstruction, tests +│ ├── evaluate.c # Static evaluation (material + PST), tests +│ ├── threads.c # pthread creation/cleanup, split-point pool, DTS helpers +│ ├── thread.c # child_thread() & root_thread() entry points, tests │ ├── ttable.c # Zobrist init, transposition table insert/find │ └── moves/ │ ├── vec.c # Dynamic move list (moves_t) management @@ -82,9 +86,13 @@ gacrux/ │ ├── extractor.py # Character-level C tokenizer (handles macros, depth tracking) │ └── analysis.js # Output data consumed by an HTML visualization ├── resources/ +│ ├── bitboards.html # Bitboard visualization / reference +│ ├── search.html # DTS paper by Hyatt et al. (Cray Blitz) │ └── uci/ │ ├── uci_min.txt # Minimal UCI spec reference -│ └── UCI-Protocol-Specification.txt # Full UCI protocol spec +│ ├── UCI-Protocol-Specification.txt # Full UCI protocol spec +│ ├── draft-1.pdf # UCI protocol draft +│ └── draft-2.pdf # UCI protocol draft ├── output/ # Build artifacts (.o files) └── opt/ # External / vendored projects (gitignored) ├── en-croissant/ # React + Tauri chess GUI @@ -133,7 +141,9 @@ The engine uses a **multi-threaded, event-driven** architecture: by `src/engine/threads.c`. Thread 0 is the `root_thread`; threads 1..N are `child_thread`s. These implement a Distributed Tree Search (DTS) pattern where the root thread owns the search tree and children collaborate on split points - (`split_point_t`). + (`split_point_t`). The root does iterative deepening with alpha-beta search, + publishing info messages per depth. Children scan the split-point pool for + published work, claim moves, search them, and share results back. - **IPC layer** (`src/ipc/`): Shared `ipc_state_t` containing an atomic `event` field that acts as a synchronization primitive. Events flow between UCI and @@ -146,6 +156,12 @@ The engine uses a **multi-threaded, event-driven** architecture: pattern) to push search info (depth, score, principal variation) to the UCI thread without locks. +- **Cooperative stop**: Engine threads check an atomic `stop` flag at each + iteration (root) or worker loop cycle (children). `threads_stop_all()` sets + the flag on all threads; `threads_join()` waits for them to exit; `threads_cleanup()` + reinitializes state. The UCI thread (which blocks on stdin) is the only thread + that uses `pthread_cancel`. + ### Position Representation Uses **64-bit bitboards** (`bitboard_t`, one per piece type × color, 12 total) @@ -173,6 +189,22 @@ stored in a `position_t` struct. Squares are indexed 0–63 (0 = a1, 7 = h1, Insertion replaces the shallowest entry when a bucket is full (replacement scheme). +### Search + +- **Alpha-beta search** (`src/engine/search.c`): Negamax with alpha-beta pruning, + TT probing for move ordering and cutoffs, aspiration windows at depth > 4. +- **Quiescence search** (`src/engine/search.c`): Stand-pat with delta pruning + (margin of 200 cp), searches captures and promotions only at depth 0. +- **PV reconstruction** (`src/engine/search.c:reconstruct_pv`): Follows TT + best-move entries to rebuild the principal variation after each iteration. + +### Evaluation + +- **Material + PST** (`src/engine/evaluate.c`): Simple material counting + (pawn=100, knight/bishop=320, rook=500, queen=900) plus piece-square table + bonuses for pawn advancement and knight centralization. Returns a score from + the side-to-move's perspective. + ### UCI Protocol - Implements the standard UCI state machine (`include/uci/internals/response.h`): @@ -185,8 +217,24 @@ stored in a `position_t` struct. Squares are indexed 0–63 (0 = a1, 7 = h1, ### Testing - **Embedded tests**: Source files use `#ifdef TEST_MOD` to conditionally compile - `test_*` functions directly into the source (e.g. `test_fen_*`, - `test_ttable_*`, `test_perft_*`, `test_uci_cmd_t`). + `test_*` functions directly into the source. Test families: + - `test_fen_*` (3) — FEN parsing + - `test_perft_*` (3) — move generation correctness against known node counts + - `test_white_king_corners`, `test_black_king_corners` — king/castling moves + - `test_uci_cmd_t`, `test_invalid_cmd_uci_cmd_t` — UCI command parsing + - `test_ttable_*` (7) — transposition table insert/find/replacement + - `test_search_*` (4) — alpha-beta search, mate detection, depth limit, stop + - `test_qsearch_finds_capture` — quiescence search captures + - `test_pv_reconstruction` — TT-based PV rebuild + - `test_eval_*` (2) — evaluation balance and material scaling + - `test_root_thread_*` (4) — root iterative deepening, depth/node limits, mate + - `test_child_*` (3) — DTS worker claim/share/stop + - `test_root_split_*`, `test_root_with_workers_*` (2) — root split with workers + - `test_threads_*` (2) — thread args initialization + - `test_split_*` (4) — split-point pool, claim, merge, share + - `test_help_scan_*` — DTS HELP protocol + - `test_thrashing_counter_*` — thrashing guard + - `test_cooperative_stop_*` — cooperative shutdown - **Test discovery** (`tests/find_tests.c`): Scours `src/` for `bool test_*()` signatures, generates `tests/generated.c` with a function-pointer table and name list. @@ -219,14 +267,22 @@ just test ## Current Status -The engine is a **work in progress**. The threaded search infrastructure (thread -creation, IPC event signaling, engine message passing, transposition table, -Zobrist hashing) and all move generation are implemented and tested. However, the -search threads themselves (`child_thread`, `root_thread`) currently perform -placeholder work (a busy-wait loop) and emit a hardcoded best move — the actual -mini-max/alpha-beta search and evaluation function are not yet implemented. The -UCI protocol layer is fully functional for position setup, options, and -`isready`/`stop`/`quit`/`go` commands. +The engine has a **working threaded search** with iterative deepening, alpha-beta +pruning, quiescence search, and a Distributed Tree Search (DTS) multi-threading +model. The root thread drives iterative deepening and publishes split points; +child workers claim moves, search them, and share results back. The UCI protocol +layer is functional for position setup, options, `isready`/`stop`/`quit`/`go` +commands, and emits `info depth N ... pv` messages with reconstructed principal +variations. The evaluation function is a simple material + PST baseline. + +**Implemented**: move generation (magic bitboards), alpha-beta search, quiescence +search, PV reconstruction, transposition table, Zobrist hashing, DTS split points +(with thrashing guard), cooperative stop, UCI protocol, and 44 tests across all +modules. + +**Remaining**: end-to-end UCI integration testing (`./build uci run` with a GUI), +deeper evaluation (tapered eval, king safety, passed pawns), time management, +node-type aware split-point selection (DTS §3.x). ## Dependencies diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 2581b5b..a02c90c 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -12,7 +12,7 @@ Multiple `.c` files are aggregated into a single translation unit by `#include`-ing them, rather than compiling separately: - `src/engine.c` pulls in `engine/threads.c`, `engine/thread.c`, - `engine/moves.c`, `engine/ttable.c`. + `engine/moves.c`, `engine/ttable.c`, `engine/evaluate.c`, `engine/search.c`. - `src/ipc.c` pulls in `ipc/go_args.c`, `ipc/moves.c`, `ipc/engine_message.c`, `ipc/state.c`, `ipc/threads.c`. - `src/main.c` and `tests/main.c` append `#include "fen.c"`, @@ -35,7 +35,6 @@ consuming `.c` file `#define`s the sentinel before including the header: Other gates in use: - `MOVES_INTERNAL` — `include/engine/moves.h` (`__forloop_*` declarations). -- `IPC_INTERNAL` — `include/ipc/state.h`. ### Test gating @@ -139,6 +138,23 @@ Every "object" type follows an `init` / `deinit` pair. ## Macros +### File-local constants + +File-scope constants that don't warrant a `#define` use anonymous enums: + +```c +enum { ROOT_MAX_DEPTH = 100, MATE_WINDOW = 1000 }; +``` + +Function-scope constants use the same pattern: + +```c +for (int depth = 1; ...) { + enum { WORKERS = 4 }; + ... +} +``` + ### Expression macros (no statement semantics) Written without surrounding parentheses, relying on call-site precedence — @@ -247,15 +263,60 @@ enum { ## Atomics -Use `<stdatomic.h>` primitives directly: +Use `<stdatomic.h>` primitives directly. The engine's threading model is +**lock-free**: no `pthread_mutex` or `pthread_cond` is used anywhere. All +inter-thread communication goes through atomics, with `sched_yield()` for +back-off in spin loops. + +### Types in use + +- `atomic_int` — flags, counters, scores (`stop`, `finished`, `best_score`, + `workers`, `next_move_idx`, `nodes_searched`). +- `atomic_bool` — boolean stop flags in `search_ctx_t` and local variables + in `child_thread`/`root_thread`. +- `atomic_flag` — spinlock for the global `engine_message_handler_t` lock. +- `atomic_uint_fast64_t` — transposition table keys. + +### Operations in use + +- `atomic_init(&x, v)` — before `pthread_create` (main thread). +- `atomic_store(&x, v)` — at thread entry or to signal state changes. +- `atomic_load(&x)` — to poll for state changes (stop signals, split-point + state). +- `atomic_fetch_add(&x, 1)` / `atomic_fetch_sub(&x, 1)` — lock-free counters + (worker registration in `split_point_t.workers`, move claiming via + `next_move_idx`). +- `atomic_compare_exchange_weak(&x, &expected, desired)` — lock-free pool + allocation (`__pool_alloc`) and best-score sharing (`split_point_share`). +- `atomic_flag_test_and_set(&flag)` / `atomic_flag_clear(&flag)` — spinlock + acquire/release for engine message handlers. + +### Spin-wait pattern + +Threads poll with `sched_yield()` in tight loops: + +```c +while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) { + sched_yield(); +} +``` + +### Two-phase initialization + +Atomics used across threads are initialized in two phases: + +1. `atomic_init(&x, v)` on the main thread before `pthread_create`. +2. `atomic_store(&x, v)` at thread entry to ensure visibility. -- `atomic_int`, `atomic_uint_fast64_t`, `atomic_flag`. -- `atomic_init(&x, v)`, `atomic_load(&x)`, `atomic_store(&x, v)`. -- `atomic_flag_test_and_set(&flag)` / `atomic_flag_clear(&flag)` for spinlocks. +### Note on `_Atomic bool` vs `atomic_bool` + +Both forms appear in the codebase: `thread.c` uses the `<stdatomic.h>` typedef +`atomic_bool` while `search.c` tests use the C11 keyword `_Atomic bool`. They +are semantically equivalent. Prefer `atomic_bool` (the typedef) for consistency. ## Test Functions -Embedded test functions: +### Signature and convention - Signature: `bool test_<thing>(void)`. - Return `true` = pass, `false` = fail. @@ -280,6 +341,54 @@ bool test_uci_cmd_t() { } ``` +### Test block structure + +Test blocks live at the bottom of source files under `#ifdef TEST_MOD` and may +need additional includes beyond the file's normal headers: + +```c +#ifdef TEST_MOD +#include <stdbool.h> +#include <stdlib.h> +#include <pthread.h> +#include "fen.h" + +// Cross-module .c inclusion for IPC types needed by tests +#include "../src/ipc/go_args.c" +#include "../src/ipc/moves.c" +#include "../src/ipc/engine_message.c" + +bool test_example() { ... } +#endif +``` + +### Anti-double-inclusion guard + +When a leaf `.c` file is compiled standalone *and* included from an aggregated +TU, use `#ifndef TEST_MOD` to prevent duplicate symbols: + +```c +#ifndef TEST_MOD +#include "moves/king.c" +#endif +``` + +### Test helper naming + +- `__double_prefix` for test helper structs: `__child_test_env_t`, + `__root_split_env_t`, `__claim_thread_args_t`. +- `__double_prefix` for test helper functions: `__root_thread_run`, + `__child_test_setup`, `__wait_split_point_done`. +- Local-scope constants via anonymous enum: `enum { WORKERS = 4 };`. + +### Thread-safety for debug counters + +Per-thread debug counters use `_Thread_local` storage to avoid races: + +```c +static _Thread_local int spins = 0; +``` + ## Build system (`build.c`) - A self-rebuilding `nob.h`-style driver. diff --git a/src/engine/thread.c b/src/engine/thread.c index 1bb9a83..bbf2605 100644 --- a/src/engine/thread.c +++ b/src/engine/thread.c @@ -70,13 +70,10 @@ void *child_thread(void *_args) { // nothing active right now: idle until the root publishes the next // iteration's block or the search is stopped if (sp == NULL) { - static _Thread_local int spins = 0; - if (++spins == 5000000) fprintf(stderr, "CHILD %d STUCK stop=%d (ptr=%p)\n", args->id, atomic_load(&args->stop), (void*)&args->stop); sched_yield(); continue; } - fprintf(stderr,"CHILD %d found sp\n",args->id); // register as a worker at this block so Merge knows the headcount atomic_fetch_add(&sp->workers, 1); @@ -174,7 +171,6 @@ void *root_thread(void *_args) { ? __pool_alloc(args->split_points, args->sp_pool_capacity) : NULL; - fprintf(stderr,"ROOT iter %d sp=%p\n",depth,(void*)sp); if (sp != NULL) { // Split (§2.4): snapshot the root node and hand the whole move list to // the pool; the root works the list alongside its workers. @@ -221,16 +217,9 @@ void *root_thread(void *_args) { if (split_point_merge(sp)) { atomic_store(&sp->state, SPLITPOINT_STATE_FREE); } - fprintf(stderr,"ROOT iter %d waiting w=%d\n",depth,atomic_load(&sp->workers)); - int spins=0; while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) { sched_yield(); - if (++spins==1000000) fprintf(stderr,"ROOT STUCK iter %d w=%d idx=%d\n",depth,atomic_load(&sp->workers),atomic_load(&sp->next_move_idx)); } - while (atomic_load(&sp->state) == SPLITPOINT_STATE_ACTIVE) { - sched_yield(); - } - fprintf(stderr,"ROOT iter %d done\n",depth); result.score = atomic_load(&sp->best_score); result.best_move = sp->best_move; @@ -11,27 +11,7 @@ #include "ipc/state.h" #include "ipc/uci.h" -void load_from_message(uci_cmd_t* cmd, const char* message) { - char token[MAX_TOKEN_SIZE]; - int i = 0, k = 0; - - while (message[i] != 0 && message[i] != '\n' && message[i] != '\r') { - if (message[i] != ' ') { - token[k++] = message[i++]; - continue; - } - if (k == 0) { - i++; - continue; - } - i++; - token[k] = 0; - uci_cmd_add(cmd, token); - k = 0; - } - token[k] = 0; - uci_cmd_add(cmd, token); -} +void load_from_message(uci_cmd_t* cmd, const char* message); // split a raw stdin chunk into lines and parse each into its own command. void load_from_buffer( diff --git a/src/uci/response.c b/src/uci/response.c index 81459d6..f3fc2d9 100644 --- a/src/uci/response.c +++ b/src/uci/response.c @@ -3,6 +3,33 @@ #include <stdlib.h> #include "uci/internals/response.h" + +// Tokenize a UCI message into a command. Supports double-quoted strings so +// that multi-word values (e.g. FEN positions) survive tokenization. +void load_from_message(uci_cmd_t* cmd, const char* message) { + char token[MAX_TOKEN_SIZE]; + int i = 0, k = 0; + bool quoted = false; + + while (message[i] != 0 && message[i] != '\n' && message[i] != '\r') { + if (message[i] == '"') { + quoted = !quoted; + i++; + continue; + } + if (message[i] == ' ' && !quoted) { + if (k == 0) { i++; continue; } + i++; + token[k] = 0; + uci_cmd_add(cmd, token); + k = 0; + continue; + } + token[k++] = message[i++]; + } + token[k] = 0; + uci_cmd_add(cmd, token); +} #include "uci/state.h" #include "fen.h" @@ -50,23 +77,30 @@ void handle_idle(uci_state_t *state, uci_cmd_t cmd) { char buffer[2048] = {0}; char name[64] = {0}; char *cursor = name; - for (int i = 1; i < cmd.args_count; i++) { - assert((long)cursor - (long)buffer < 2048); + int limit = 64; + int i = 1; + if (i < cmd.args_count && strcmp(uci_cmd_get_arg(cmd, i), "name") == 0) i++; + for (; i < cmd.args_count; i++) { + assert((long)cursor - (long)(limit == 64 ? name : buffer) < limit); char *arg = uci_cmd_get_arg(cmd, i); if (strcmp(arg, "value") == 0) { - *(--cursor) = 0; + if (cursor > name && *(cursor - 1) == ' ') cursor--; + *cursor = 0; cursor = buffer; + limit = 2048; continue; } - cursor = stpncpy(cursor, arg, MAX_TOKEN_SIZE); + while (*arg) *(cursor++) = *(arg++); *(cursor++) = ' '; } - *(--cursor) = 0; + if (cursor > name && *(cursor - 1) == ' ') cursor--; + if (cursor > buffer && *(cursor - 1) == ' ') cursor--; + *cursor = 0; apply_option(state, name, buffer); } else if (strcmp(cmd.root, "ucinewgame") == 0) { } else if (strcmp(cmd.root, "position") == 0) { int k = 0; - char* mode = uci_cmd_get_arg(cmd, 0); + char* mode = uci_cmd_get_arg(cmd, k++); if (strcmp(mode, "startpos") == 0) { struct fen_load fen = load_fen( "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" @@ -83,13 +117,8 @@ void handle_idle(uci_state_t *state, uci_cmd_t cmd) { state->position = fen.position; } else { assert(0 && "Invalid argument for position command"); } - if (cmd.args_count >= k) return; + if (cmd.args_count <= k) return; assert(strcmp(uci_cmd_get_arg(cmd, k++), "moves") == 0); - assert(state->moves->count <= cmd.args_count - k); - int old_move_count = state->moves->count; - state->moves->count = cmd.args_count - k; - - k += old_move_count; for (; k < cmd.args_count; k++) { char* move = uci_cmd_get_arg(cmd, k); add_ipc_move( @@ -102,15 +131,6 @@ void handle_idle(uci_state_t *state, uci_cmd_t cmd) { } } else if (strcmp(cmd.root, "stop") == 0) { } else if (strcmp(cmd.root, "quit") == 0) { - free(cmd.args); - - int settings_count = sizeof(state->option_settings)/sizeof(option_setting_t); - for (int i = 0; i < settings_count; i++) { - option_setting_t setting = state->option_settings[i]; - if (OPTION_STRING != setting.type) continue; - free(setting.value.string->data); - } - state->signal = UCI_SIGNAL_QUIT; } else if (strcmp(cmd.root, "isready") == 0) { state->current_state = UCI_STATE_SYNC; @@ -210,3 +230,600 @@ void handle_halt(uci_state_t *state, uci_cmd_t cmd) { // --- state->current_state = UCI_STATE_IDLE; } + +#ifdef TEST_MOD +#include <stdbool.h> +#include <unistd.h> + +static uci_cmd_t __make_cmd(const char *line) { + uci_cmd_t cmd; + uci_cmd_init(&cmd); + load_from_message(&cmd, line); + return cmd; +} + +static void __free_cmd(uci_cmd_t cmd) { uci_cmd_deinit(cmd); } + +static char *__capture_uci( + void (*handler)(uci_state_t *, uci_cmd_t), + uci_state_t *state, + const char *line, + char *out, + int out_size +) { + int pipefd[2]; + if (pipe(pipefd) != 0) { out[0] = 0; return out; } + int saved = dup(STDOUT_FILENO); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + + uci_cmd_t cmd = __make_cmd(line); + handler(state, cmd); + __free_cmd(cmd); + + fflush(stdout); + dup2(saved, STDOUT_FILENO); + close(saved); + + int n = read(pipefd[0], out, out_size - 1); + close(pipefd[0]); + out[n > 0 ? n : 0] = 0; + return out; +} + +// ---- UCI handshake ---- + +bool test_uci_handshake_id_and_uciok() { + uci_state_t state; + uci_state_init(&state); + + char out[2048]; + __capture_uci(handle_initial, &state, "uci", out, sizeof(out)); + + bool ok = true; + ok &= strstr(out, "id name Gacrux") != NULL; + ok &= strstr(out, "id author") != NULL; + ok &= strstr(out, "uciok") != NULL; + ok &= state.current_state == UCI_STATE_IDLE; + + uci_state_deinit(&state); + return ok; +} + +bool test_uci_handshake_prints_all_options() { + uci_state_t state; + uci_state_init(&state); + + char out[4096]; + __capture_uci(handle_initial, &state, "uci", out, sizeof(out)); + + bool ok = true; + ok &= strstr(out, "option name Threads type spin") != NULL; + ok &= strstr(out, "option name Hash type spin") != NULL; + ok &= strstr(out, "option name Clear Hash type button") != NULL; + ok &= strstr(out, "option name UCI_ShowCurrLine type check") != NULL; + ok &= strstr(out, "option name UCI_LimitStrength type check") != NULL; + ok &= strstr(out, "option name UCI_Elo type spin") != NULL; + ok &= strstr(out, "option name UCI_AnalyseMode type check") != NULL; + ok &= strstr(out, "option name UCI_Opponent type string") != NULL; + ok &= strstr(out, "option name UCI_EngineAbout type string") != NULL; + + uci_state_deinit(&state); + return ok; +} + +// ---- isready ---- + +bool test_isready_returns_readyok() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_sync, &state, "isready", out, sizeof(out)); + + bool ok = true; + ok &= strstr(out, "readyok") != NULL; + ok &= state.current_state == UCI_STATE_IDLE; + + uci_state_deinit(&state); + return ok; +} + +bool test_isready_transitions_to_idle() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_SYNC; + + char out[256]; + __capture_uci(handle_uci, &state, "isready", out, sizeof(out)); + + bool ok = true; + ok &= strstr(out, "readyok") != NULL; + ok &= state.current_state == UCI_STATE_IDLE; + + uci_state_deinit(&state); + return ok; +} + +// ---- debug ---- + +bool test_debug_on_sets_flag() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "debug on", out, sizeof(out)); + + bool ok = state.debug == true; + uci_state_deinit(&state); + return ok; +} + +bool test_debug_off_clears_flag() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + state.debug = true; + + char out[256]; + __capture_uci(handle_idle, &state, "debug off", out, sizeof(out)); + + bool ok = state.debug == false; + uci_state_deinit(&state); + return ok; +} + +// ---- setoption ---- + +bool test_setoption_spin() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name Threads value 4", out, sizeof(out)); + + bool ok = state.threads == 4; + uci_state_deinit(&state); + return ok; +} + +bool test_setoption_spin_clamps_too_low() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name Threads value 0", out, sizeof(out)); + + bool ok = state.threads == 1; + uci_state_deinit(&state); + return ok; +} + +bool test_setoption_spin_clamps_too_high() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name Threads value 99", out, sizeof(out)); + + bool ok = state.threads == 1; + + uci_state_deinit(&state); + return ok; +} + +bool test_setoption_check_true() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name UCI_AnalyseMode value true", out, sizeof(out)); + + bool ok = state.uci_analysemode == true; + uci_state_deinit(&state); + return ok; +} + +bool test_setoption_check_false() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + state.uci_analysemode = true; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name UCI_AnalyseMode value false", out, sizeof(out)); + + bool ok = state.uci_analysemode == false; + uci_state_deinit(&state); + return ok; +} + +bool test_setoption_button() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name Clear Hash", out, sizeof(out)); + + bool ok = state.clear_hash == true; + uci_state_deinit(&state); + return ok; +} + +bool test_setoption_string() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name UCI_Opponent value Magnus", out, sizeof(out)); + + bool ok = true; + ok &= state.uci_opponent.length == 6; + ok &= memcmp(state.uci_opponent.data, "Magnus", 6) == 0; + + uci_state_deinit(&state); + return ok; +} + +bool test_setoption_hash_changes_value() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "setoption name Hash value 128", out, sizeof(out)); + + bool ok = state.hash == 128; + uci_state_deinit(&state); + return ok; +} + +// ---- position ---- + +bool test_position_startpos_loads() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + ipc_moves_t moves; + ipc_moves_init(&moves); + state.moves = &moves; + + char out[256]; + __capture_uci(handle_idle, &state, + "position startpos", out, sizeof(out)); + + bool ok = true; + ok &= state.position.bitboards[WHITE_PAWN] != 0; + ok &= state.position.turn == WHITE_TURN; + + ipc_moves_deinit(&moves); + uci_state_deinit(&state); + return ok; +} + +bool test_position_fen_loads() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + ipc_moves_t moves; + ipc_moves_init(&moves); + state.moves = &moves; + + char out[256]; + __capture_uci(handle_idle, &state, + "position fen \"k7/8/KQ6/8/8/8/8/8 w - - 0 1\"", out, sizeof(out)); + + bool ok = state.position.bitboards[WHITE_QUEEN] != 0; + + ipc_moves_deinit(&moves); + uci_state_deinit(&state); + return ok; +} + +bool test_position_startpos_moves() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + ipc_moves_t moves; + ipc_moves_init(&moves); + state.moves = &moves; + + char out[256]; + __capture_uci(handle_idle, &state, + "position startpos moves e2e4", out, sizeof(out)); + + bool ok = state.moves->count == 1; + if (ok) { + ok &= state.moves->data[0].from == 33; + ok &= state.moves->data[0].to == 35; + } + + ipc_moves_deinit(&moves); + uci_state_deinit(&state); + return ok; +} + +// ---- go ---- + +bool test_go_depth_sets_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go depth 10", out, sizeof(out)); + + bool ok = true; + ok &= state.go_args.depth == 10; + ok &= state.current_state == UCI_STATE_ACTIVE; + ok &= state.signal == UCI_SIGNAL_GO; + + uci_state_deinit(&state); + return ok; +} + +bool test_go_wtime_btime_sets_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "go wtime 60000 btime 60000 winc 1000 binc 1000", out, sizeof(out)); + + bool ok = true; + ok &= state.go_args.wtime == 60000; + ok &= state.go_args.btime == 60000; + ok &= state.go_args.winc == 1000; + ok &= state.go_args.binc == 1000; + + uci_state_deinit(&state); + return ok; +} + +bool test_go_nodes_sets_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go nodes 50000", out, sizeof(out)); + + bool ok = state.go_args.nodes == 50000; + uci_state_deinit(&state); + return ok; +} + +bool test_go_movetime_sets_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go movetime 5000", out, sizeof(out)); + + bool ok = state.go_args.movetime == 5000; + uci_state_deinit(&state); + return ok; +} + +bool test_go_movestogo_sets_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go movestogo 30", out, sizeof(out)); + + bool ok = state.go_args.movestogo == 30; + uci_state_deinit(&state); + return ok; +} + +bool test_go_mate_sets_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go mate 3", out, sizeof(out)); + + bool ok = state.go_args.mate == 3; + uci_state_deinit(&state); + return ok; +} + +bool test_go_infinite_sets_flag() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go infinite", out, sizeof(out)); + + bool ok = state.go_args.infinite == true; + uci_state_deinit(&state); + return ok; +} + +bool test_go_ponder_sets_flag() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go ponder", out, sizeof(out)); + + bool ok = state.go_args.ponder == true; + uci_state_deinit(&state); + return ok; +} + +bool test_go_combined_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, + "go depth 6 wtime 30000 btime 30000 winc 500 movestogo 20", out, sizeof(out)); + + bool ok = true; + ok &= state.go_args.depth == 6; + ok &= state.go_args.wtime == 30000; + ok &= state.go_args.btime == 30000; + ok &= state.go_args.winc == 500; + ok &= state.go_args.movestogo == 20; + + uci_state_deinit(&state); + return ok; +} + +bool test_go_perft_sets_args() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "go perft 5", out, sizeof(out)); + + bool ok = state.go_args.perft == 5; + uci_state_deinit(&state); + return ok; +} + +// ---- state machine transitions ---- + +bool test_active_stop_transitions_to_idle() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_ACTIVE; + + char out[256]; + __capture_uci(handle_active, &state, "stop", out, sizeof(out)); + + bool ok = true; + ok &= state.current_state == UCI_STATE_HALT; + + uci_state_deinit(&state); + return ok; +} + +bool test_halt_sets_stop_signal() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_HALT; + + char out[256]; + __capture_uci(handle_halt, &state, "", out, sizeof(out)); + + bool ok = true; + ok &= state.signal == UCI_SIGNAL_STOP; + ok &= state.current_state == UCI_STATE_IDLE; + + uci_state_deinit(&state); + return ok; +} + +bool test_active_isready_transitions_to_ping() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_ACTIVE; + + char out[256]; + __capture_uci(handle_active, &state, "isready", out, sizeof(out)); + + bool ok = state.current_state == UCI_STATE_PING; + + uci_state_deinit(&state); + return ok; +} + +bool test_quit_sets_signal() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "quit", out, sizeof(out)); + + bool ok = state.signal == UCI_SIGNAL_QUIT; + uci_state_deinit(&state); + return ok; +} + +bool test_new_game_accepted() { + uci_state_t state; + uci_state_init(&state); + state.current_state = UCI_STATE_IDLE; + + char out[256]; + __capture_uci(handle_idle, &state, "ucinewgame", out, sizeof(out)); + + bool ok = state.current_state == UCI_STATE_IDLE; + uci_state_deinit(&state); + return ok; +} + +bool test_full_handshake_sequence() { + uci_state_t state; + uci_state_init(&state); + + ipc_moves_t moves; + ipc_moves_init(&moves); + state.moves = &moves; + + char out[2048]; + + __capture_uci(handle_uci, &state, "uci", out, sizeof(out)); + bool ok = state.current_state == UCI_STATE_IDLE; + ok &= strstr(out, "uciok") != NULL; + + __capture_uci(handle_idle, &state, "isready", out, sizeof(out)); + ok &= state.current_state == UCI_STATE_SYNC; + + __capture_uci(handle_uci, &state, "isready", out, sizeof(out)); + ok &= state.current_state == UCI_STATE_IDLE; + ok &= strstr(out, "readyok") != NULL; + + __capture_uci(handle_idle, &state, + "setoption name Threads value 2", out, sizeof(out)); + ok &= state.threads == 2; + + __capture_uci(handle_idle, &state, "position startpos", out, sizeof(out)); + ok &= state.position.bitboards[WHITE_PAWN] != 0; + + __capture_uci(handle_idle, &state, "go depth 5", out, sizeof(out)); + ok &= state.current_state == UCI_STATE_ACTIVE; + ok &= state.go_args.depth == 5; + + __capture_uci(handle_active, &state, "stop", out, sizeof(out)); + ok &= state.current_state == UCI_STATE_HALT; + + __capture_uci(handle_halt, &state, "", out, sizeof(out)); + ok &= state.current_state == UCI_STATE_IDLE; + + ipc_moves_deinit(&moves); + uci_state_deinit(&state); + return ok; +} +#endif diff --git a/src/uci/state.c b/src/uci/state.c index 183f849..e108492 100644 --- a/src/uci/state.c +++ b/src/uci/state.c @@ -1,3 +1,4 @@ +#include <stdlib.h> #include <string.h> #include "uci/state.h" @@ -180,7 +181,16 @@ void uci_state_init(uci_state_t *state) { ); } -void uci_state_deinit(uci_state_t *state) {} +void uci_state_deinit(uci_state_t *state) { + int settings_count = sizeof(state->option_settings)/sizeof(option_setting_t); + for (int i = 0; i < settings_count; i++) { + option_setting_t *s = &state->option_settings[i]; + if (s->type == OPTION_STRING && s->value.string && s->value.string->data) { + free(s->value.string->data); + s->value.string->data = NULL; + } + } +} void apply_option(uci_state_t *state, char *name, char *buffer) { int settings_count = sizeof(state->option_settings)/sizeof(option_setting_t); @@ -218,3 +228,147 @@ void apply_option(uci_state_t *state, char *name, char *buffer) { } } +#ifdef TEST_MOD +#include <stdbool.h> + +bool test_uci_state_init_defaults() { + uci_state_t state; + uci_state_init(&state); + + bool ok = true; + ok &= state.signal == UCI_SIGNAL_NONE; + ok &= state.current_state == UCI_STATE_INITIAL; + ok &= state.threads == 1; + ok &= state.hash == 256; + ok &= state.clear_hash == false; + ok &= state.uci_showcurrline == false; + ok &= state.uci_showrefutations == false; + ok &= state.uci_limitstrength == false; + ok &= state.uci_elo == 800; + ok &= state.uci_analysemode == false; + ok &= strcmp(state.name, "Gacrux") == 0; + ok &= strcmp(state.author, "Aargh Rai <aarghrai.com>") == 0; + + uci_state_deinit(&state); + return ok; +} + +bool test_apply_option_spin_valid() { + uci_state_t state; + uci_state_init(&state); + + apply_option(&state, "Threads", "4"); + bool ok = state.threads == 4; + + apply_option(&state, "Hash", "128"); + ok &= state.hash == 128; + + uci_state_deinit(&state); + return ok; +} + +bool test_apply_option_spin_below_min() { + uci_state_t state; + uci_state_init(&state); + + apply_option(&state, "Threads", "0"); + bool ok = state.threads == 1; + + uci_state_deinit(&state); + return ok; +} + +bool test_apply_option_spin_above_max() { + uci_state_t state; + uci_state_init(&state); + + apply_option(&state, "Threads", "100"); + bool ok = state.threads == 1; + + uci_state_deinit(&state); + return ok; +} + +bool test_apply_option_check() { + uci_state_t state; + uci_state_init(&state); + + apply_option(&state, "UCI_AnalyseMode", "true"); + bool ok = state.uci_analysemode == true; + + apply_option(&state, "UCI_AnalyseMode", "false"); + ok &= state.uci_analysemode == false; + + uci_state_deinit(&state); + return ok; +} + +bool test_apply_option_button() { + uci_state_t state; + uci_state_init(&state); + + apply_option(&state, "Clear Hash", ""); + bool ok = state.clear_hash == true; + + uci_state_deinit(&state); + return ok; +} + +bool test_apply_option_string() { + uci_state_t state; + uci_state_init(&state); + + apply_option(&state, "UCI_Opponent", "Carlsen"); + bool ok = true; + ok &= state.uci_opponent.length == 7; + ok &= memcmp(state.uci_opponent.data, "Carlsen", 7) == 0; + + uci_state_deinit(&state); + return ok; +} + +bool test_apply_option_unknown_ignored() { + uci_state_t state; + uci_state_init(&state); + state.debug = false; + + apply_option(&state, "Nonexistent", "42"); + bool ok = true; + + uci_state_deinit(&state); + return ok; +} + +bool test_option_settings_count() { + uci_state_t state; + uci_state_init(&state); + + int count = sizeof(state.option_settings) / sizeof(option_setting_t); + bool ok = count == 11; + + uci_state_deinit(&state); + return ok; +} + +bool test_option_settings_types() { + uci_state_t state; + uci_state_init(&state); + + bool ok = true; + ok &= state.option_settings[0].type == OPTION_SPIN; // Threads + ok &= state.option_settings[1].type == OPTION_SPIN; // Hash + ok &= state.option_settings[2].type == OPTION_BUTTON; // Clear Hash + ok &= state.option_settings[3].type == OPTION_CHECK; // UCI_ShowCurrLine + ok &= state.option_settings[4].type == OPTION_CHECK; // UCI_ShowRefutations + ok &= state.option_settings[5].type == OPTION_CHECK; // UCI_LimitStrength + ok &= state.option_settings[6].type == OPTION_SPIN; // UCI_Elo + ok &= state.option_settings[7].type == OPTION_CHECK; // UCI_AnalyseMode + ok &= state.option_settings[8].type == OPTION_STRING; // UCI_Opponent + ok &= state.option_settings[9].type == OPTION_STRING; // UCI_EngineAbout + ok &= state.option_settings[10].type == OPTION_STRING;// UCI_SetPositionValue + + uci_state_deinit(&state); + return ok; +} +#endif + diff --git a/tests/generated.c b/tests/generated.c index fc10446..3789b0b 100644 --- a/tests/generated.c +++ b/tests/generated.c @@ -7,9 +7,11 @@ #include "../src/engine/moves.c" #include "../src/engine/moves/king.c" #include "../src/uci/command.c" +#include "../src/uci/response.c" +#include "../src/uci/state.c" -int total_test_count = 43; -bool (*tests[43])(void) = { +int total_test_count = 86; +bool (*tests[86])(void) = { test_fen_no_passant, test_fen_passant, test_starting_position, @@ -53,10 +55,53 @@ bool (*tests[43])(void) = { test_black_king_corners, test_uci_cmd_t, test_invalid_cmd_uci_cmd_t, + test_uci_handshake_id_and_uciok, + test_uci_handshake_prints_all_options, + test_isready_returns_readyok, + test_isready_transitions_to_idle, + test_debug_on_sets_flag, + test_debug_off_clears_flag, + test_setoption_spin, + test_setoption_spin_clamps_too_low, + test_setoption_spin_clamps_too_high, + test_setoption_check_true, + test_setoption_check_false, + test_setoption_button, + test_setoption_string, + test_setoption_hash_changes_value, + test_position_startpos_loads, + test_position_fen_loads, + test_position_startpos_moves, + test_go_depth_sets_args, + test_go_wtime_btime_sets_args, + test_go_nodes_sets_args, + test_go_movetime_sets_args, + test_go_movestogo_sets_args, + test_go_mate_sets_args, + test_go_infinite_sets_flag, + test_go_ponder_sets_flag, + test_go_combined_args, + test_go_perft_sets_args, + test_active_stop_transitions_to_idle, + test_halt_sets_stop_signal, + test_active_isready_transitions_to_ping, + test_quit_sets_signal, + test_new_game_accepted, + test_full_handshake_sequence, + test_uci_state_init_defaults, + test_apply_option_spin_valid, + test_apply_option_spin_below_min, + test_apply_option_spin_above_max, + test_apply_option_check, + test_apply_option_button, + test_apply_option_string, + test_apply_option_unknown_ignored, + test_option_settings_count, + test_option_settings_types, }; int max_test_name_size = 35; -char test_names[43][100] = { +char test_names[86][100] = { "fen_no_passant", "fen_passant", "starting_position", @@ -100,4 +145,47 @@ char test_names[43][100] = { "black_king_corners", "uci_cmd_t", "invalid_cmd_uci_cmd_t", + "uci_handshake_id_and_uciok", + "uci_handshake_prints_all_options", + "isready_returns_readyok", + "isready_transitions_to_idle", + "debug_on_sets_flag", + "debug_off_clears_flag", + "setoption_spin", + "setoption_spin_clamps_too_low", + "setoption_spin_clamps_too_high", + "setoption_check_true", + "setoption_check_false", + "setoption_button", + "setoption_string", + "setoption_hash_changes_value", + "position_startpos_loads", + "position_fen_loads", + "position_startpos_moves", + "go_depth_sets_args", + "go_wtime_btime_sets_args", + "go_nodes_sets_args", + "go_movetime_sets_args", + "go_movestogo_sets_args", + "go_mate_sets_args", + "go_infinite_sets_flag", + "go_ponder_sets_flag", + "go_combined_args", + "go_perft_sets_args", + "active_stop_transitions_to_idle", + "halt_sets_stop_signal", + "active_isready_transitions_to_ping", + "quit_sets_signal", + "new_game_accepted", + "full_handshake_sequence", + "uci_state_init_defaults", + "apply_option_spin_valid", + "apply_option_spin_below_min", + "apply_option_spin_above_max", + "apply_option_check", + "apply_option_button", + "apply_option_string", + "apply_option_unknown_ignored", + "option_settings_count", + "option_settings_types", }; |
