summaryrefslogtreecommitdiff
path: root/src/engine
diff options
context:
space:
mode:
authorAargh Rai <aargh.rai+git@gmail.com>2026-08-27 17:41:07 +0530
committerAargh Rai <aargh.rai+git@gmail.com>2026-08-27 17:41:07 +0530
commit5a02743c0c9563f41a8c3821a7bbce4fc3b0b839 (patch)
treef36aa805b8c90a8d786e8561a9d84f57e6191c19 /src/engine
parentd87b4744bed8f0c43e11ea12508ef2941bdd7186 (diff)
bug fixes
Diffstat (limited to 'src/engine')
-rw-r--r--src/engine/search.c160
-rw-r--r--src/engine/thread.c51
-rw-r--r--src/engine/threads.c105
3 files changed, 299 insertions, 17 deletions
diff --git a/src/engine/search.c b/src/engine/search.c
index 41e004c..0744d70 100644
--- a/src/engine/search.c
+++ b/src/engine/search.c
@@ -17,6 +17,90 @@ static bool search_should_stop(search_ctx_t *ctx) {
return false;
}
+// material value of a captured piece, used for delta pruning in qsearch.
+static i32 piece_capture_value(position_t position, square_t to_sq) {
+ for (int p = 0; p < PIECE_TYPE_COUNT; p++) {
+ if (occupied_by(position.bitboards[p], to_sq)) {
+ switch (p) {
+ case WHITE_PAWN: case BLACK_PAWN: return 100;
+ case WHITE_KNIGHT: case BLACK_KNIGHT: return 320;
+ case WHITE_BISHOP: case BLACK_BISHOP: return 320;
+ case WHITE_ROOK: case BLACK_ROOK: return 500;
+ case WHITE_QUEEN: case BLACK_QUEEN: return 900;
+ default: return 0;
+ }
+ }
+ }
+ return 0;
+}
+
+// quiescence search: only captures and promotions, until stand-pat or no
+// capture improves alpha. Avoids the horizon effect at depth 0.
+static search_result_t qsearch(
+ position_t position,
+ int alpha,
+ int beta,
+ search_ctx_t *ctx
+) {
+ if (ctx != NULL && ctx->nodes != NULL) (*ctx->nodes)++;
+ if (search_should_stop(ctx)) {
+ return (search_result_t) { .score = 0, .best_move = (move_t){0} };
+ }
+
+ i32 stand_pat = evaluate(position);
+ if (stand_pat >= beta) {
+ return (search_result_t) { .score = beta, .best_move = (move_t){0} };
+ }
+ if (stand_pat > alpha) alpha = stand_pat;
+
+ // generate only captures (and promotions)
+ moves_t moves;
+ moves_init(&moves);
+ get_legal_moves(&moves, position);
+
+ // filter to captures/promotions only
+ move_t cap_moves[64];
+ int cap_count = 0;
+ for (u32 i = 0; i < moves.length; i++) {
+ move_t m = moves.moves[i];
+ bool is_capture = (m.flags & MOVE_CAPTURE) || (m.flags & MOVE_EN_PASSANT);
+ bool is_promo = m.flags & (MOVE_PROMOTE_Q | MOVE_PROMOTE_R | MOVE_PROMOTE_B | MOVE_PROMOTE_N);
+ if (is_capture || is_promo) {
+ cap_moves[cap_count++] = m;
+ }
+ }
+ moves_deinit(moves);
+
+ if (cap_count == 0) {
+ return (search_result_t) { .score = stand_pat, .best_move = (move_t){0} };
+ }
+
+ search_result_t result = { .score = stand_pat, .best_move = cap_moves[0] };
+
+ for (int i = 0; i < cap_count; i++) {
+ move_t m = cap_moves[i];
+
+ // delta pruning: if the capture can't possibly raise alpha, skip it.
+ // (largest possible gain is queen ~900; add a safety margin.)
+ i32 gain = piece_capture_value(position, m.to);
+ if (stand_pat + gain + 200 < alpha) continue;
+
+ position_t child = position;
+ position_make_move(&child, m);
+ search_result_t child_res = qsearch(child, -beta, -alpha, ctx);
+ i32 score = -child_res.score;
+
+ if (score > result.score) {
+ result.score = score;
+ result.best_move = m;
+ }
+ if (score > alpha) alpha = score;
+ if (alpha >= beta) break;
+ }
+
+ return result;
+}
+
search_result_t search(
position_t position,
int depth,
@@ -35,7 +119,7 @@ search_result_t search(
}
if (depth <= 0) {
- return (search_result_t) { .score = evaluate(position) };
+ return qsearch(position, alpha, beta, ctx);
}
moves_t moves;
@@ -122,6 +206,30 @@ search_result_t search(
return result;
}
+// Reconstruct the principal variation by following TT best-move entries.
+int reconstruct_pv(
+ position_t position,
+ ttable_t tt,
+ move_t *pv_out,
+ int max_len
+) {
+ int count = 0;
+ position_t pos = position;
+ // guard against cycles: at most 128 plies
+ for (int i = 0; i < max_len && i < 128; i++) {
+ u64 key = zobrist_hash(pos);
+ tentry_t *entry = ttable_find(tt, key);
+ if (entry == NULL) break;
+
+ move_t m = entry->best_move;
+ if (m.from == 0 && m.to == 0) break;
+
+ pv_out[count++] = m;
+ position_make_move(&pos, m);
+ }
+ return count;
+}
+
#ifdef TEST_MOD
#include <stdbool.h>
#include <stdlib.h>
@@ -203,4 +311,54 @@ bool test_search_stop_signal_halts() {
// with a 1-node budget the search must bail early, not run to depth 6.
return nodes <= 2 && res.score == 0;
}
+
+bool test_qsearch_finds_capture() {
+ // position where white can capture a queen: white pawn on d4, black queen on e5.
+ struct fen_load r = load_fen("rnb1kbnr/pppp1ppp/8/4q3/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 1");
+ if (r.failed) return false;
+
+ ttable_t tt;
+ ttable_init(&tt, 2);
+
+ _Atomic bool stop = false;
+ go_args_t go = {0};
+ u64 nodes = 0;
+ search_ctx_t ctx = { .ttable = &tt, .go_args = &go, .stop = &stop, .nodes = &nodes };
+
+ // depth 1: the search should see the dxe5 capture via qsearch
+ search_result_t res = search(r.position, 1, -SEARCH_INF, SEARCH_INF, &ctx);
+ ttable_deinit(tt);
+
+ // dxe5 captures the queen, so the best move must be d4->e5
+ bool found_capture = (res.best_move.from == 35 && res.best_move.to == 44); // d4=35, e5=44
+ // even if the move ordering differs, the score should be strongly positive
+ return res.score > 500;
+}
+
+bool test_pv_reconstruction() {
+ position_t pos = position_starting();
+ ttable_t tt;
+ ttable_init(&tt, 4);
+
+ _Atomic bool stop = false;
+ go_args_t go = {0};
+ u64 nodes = 0;
+ search_ctx_t ctx = { .ttable = &tt, .go_args = &go, .stop = &stop, .nodes = &nodes };
+
+ // run a shallow search to populate the TT
+ search(pos, 4, -SEARCH_INF, SEARCH_INF, &ctx);
+
+ // reconstruct PV from the TT
+ move_t pv[32];
+ int pv_len = reconstruct_pv(pos, tt, pv, 32);
+ ttable_deinit(tt);
+
+ // PV should have at least 1 move (the root best move)
+ if (pv_len < 1) return false;
+ // every PV move should be on-board
+ for (int i = 0; i < pv_len; i++) {
+ if (pv[i].from >= 64 || pv[i].to >= 64) return false;
+ }
+ return true;
+}
#endif
diff --git a/src/engine/thread.c b/src/engine/thread.c
index 5891558..1bb9a83 100644
--- a/src/engine/thread.c
+++ b/src/engine/thread.c
@@ -40,7 +40,7 @@ void threads_go(struct threads *handler) {
void *child_thread(void *_args) {
struct child_thread_args *args = (struct child_thread_args*)_args;
- args->stop = 0;
+ atomic_store(&args->stop, 0);
atomic_bool stop = false;
u64 nodes = 0;
@@ -71,7 +71,7 @@ void *child_thread(void *_args) {
// 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\n", args->id, atomic_load(&args->stop));
+ 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;
}
@@ -89,8 +89,14 @@ void *child_thread(void *_args) {
position_t child = sp->position_snapshot;
position_make_move(&child, move);
+ u64 prev_nodes = nodes;
search_result_t res = search(child, sp->depth - 1, alpha, beta, &ctx);
+ // §2.1 thrashing guard: track how many nodes were searched at this
+ // split point so that split_point_worth_sharing() can refuse HELP
+ // on very shallow / barely-searched blocks.
+ atomic_fetch_add(&sp->nodes_searched, (int)(nodes - prev_nodes));
+
// Share (§2.8): negamax — our value flips sign for the parent side
split_point_share(sp, -res.score, move);
if (-res.score > best_score) {
@@ -125,7 +131,7 @@ void *child_thread(void *_args) {
// tree
void *root_thread(void *_args) {
struct root_thread_args *args = (struct root_thread_args*)_args;
- args->stop = 0;
+ atomic_store(&args->stop, 0);
atomic_bool stop = false;
u64 nodes = 0;
@@ -149,6 +155,12 @@ void *root_thread(void *_args) {
// iterative deepening: search at increasing depths, publishing an info
// message per completed depth so the UCI thread can emit `info ... pv`.
for (int depth = 1; depth <= max_depth; depth++) {
+ // propagate external stop signal into the search context so that
+ // search_should_stop() picks it up at the next node.
+ if (atomic_load(&args->stop)) {
+ atomic_store(&stop, true);
+ break;
+ }
if (atomic_load(&stop)) break;
// root move list (DTS §3.4: splitting at the root is always safe/PV)
@@ -156,7 +168,8 @@ void *root_thread(void *_args) {
moves_init(&moves);
get_legal_moves(&moves, *args->position);
- bool can_split = moves.length > 1 && args->split_points != NULL;
+ bool can_split = moves.length > 1 && args->split_points != NULL
+ && depth >= 3; // §2.1 thrashing guard: don't split at depth < 3
split_point_t *sp = can_split
? __pool_alloc(args->split_points, args->sp_pool_capacity)
: NULL;
@@ -221,6 +234,7 @@ void *root_thread(void *_args) {
result.score = atomic_load(&sp->best_score);
result.best_move = sp->best_move;
+ moves_deinit(sp->moves);
// the move list now belongs to the pool block (released on its next
// reuse), so there is nothing to free here
} else {
@@ -230,17 +244,28 @@ void *root_thread(void *_args) {
}
if (atomic_load(&stop)) break;
+ if (atomic_load(&args->stop)) {
+ atomic_store(&stop, true);
+ break;
+ }
message->depth = depth;
message->nodes = nodes > INT_MAX ? INT_MAX : (int)nodes;
message->score_cp = result.score;
message->mate = __score_to_mate(result.score);
- // pv is already initialized by engine_message_init / handler_push.
message->pv.count = 0;
- add_ipc_move(&message->pv, (ipc_move_t) {
- result.best_move.from,
- result.best_move.to,
- });
+ move_t pv_buf[32];
+ int pv_len = reconstruct_pv(*args->position, *args->ttable, pv_buf, 32);
+ if (pv_len > 0) {
+ for (int i = 0; i < pv_len; i++) {
+ add_ipc_move(&message->pv, (ipc_move_t) { pv_buf[i].from, pv_buf[i].to });
+ }
+ } else {
+ add_ipc_move(&message->pv, (ipc_move_t) {
+ result.best_move.from,
+ result.best_move.to,
+ });
+ }
if (!should_continue(message, args->go_args)) {
break;
@@ -257,8 +282,8 @@ void *root_thread(void *_args) {
engine_message_mark_ready(message);
// signal workers to stop their loops, then mark this thread as finished.
- for (int i = 0; i < args->workers_count; i++) {
- atomic_store(&args->workers_stop[i], 1);
+ for (int i = 1; i <= args->workers_count; i++) {
+ atomic_store(&args->handler->args[i].child_thread_args.stop, 1);
}
atomic_store(&args->stop, 1);
atomic_store(&args->finished, 1);
@@ -291,6 +316,7 @@ static bool __root_thread_run(
ttable_t tt;
ttable_init(&tt, ttable_log);
+ args->ttable = &tt;
root_thread(args);
@@ -350,7 +376,7 @@ bool test_root_thread_emits_info_and_bestmove() {
// every published message must have a valid depth and on-board pv move.
for (engine_message_t *m = messages; m != NULL; m = m->next) {
ok &= m->depth >= 1 && m->depth <= 3;
- ok &= m->pv.count == 1;
+ ok &= m->pv.count >= 1;
ok &= m->pv.data[0].from >= 0 && m->pv.data[0].from < 64;
ok &= m->pv.data[0].to >= 0 && m->pv.data[0].to < 64;
}
@@ -694,6 +720,7 @@ static void __root_split_env_deinit(
engine_message_handler_deinit(&env->message_handlers[i]);
}
ttable_deinit(env->handler.ttable);
+ threads_cleanup(&env->handler);
threads_deinit(&env->handler);
}
diff --git a/src/engine/threads.c b/src/engine/threads.c
index fe38013..060afb9 100644
--- a/src/engine/threads.c
+++ b/src/engine/threads.c
@@ -73,11 +73,24 @@ bool threads_all_done(struct threads *handler) {
return true;
}
+void threads_stop_all(struct threads *handler) {
+ int running_count = handler->running_count;
+ for (int i = 0; i < running_count; i++) {
+ atomic_store(&handler->args[i].root_thread_args.stop, 1);
+ }
+}
+
+void threads_join(struct threads *handler) {
+ int running_count = handler->running_count;
+ for (int i = 0; i < running_count; i++) {
+ pthread_join(handler->threads[i], NULL);
+ }
+}
+
void threads_cleanup(struct threads *handler) {
int running_count = handler->running_count;
for (int i = 0; i < running_count; i++) {
__threads_args_init(handler->args + i, handler, i);
- pthread_cancel(handler->threads[i]);
}
handler->running_count = 0;
}
@@ -111,9 +124,7 @@ void __threads_args_init(
args->ttable = &handler->ttable;
args->split_points = handler->split_point_pool;
args->sp_pool_capacity = handler->capacity;
- args->workers_stop = handler->running_count > 1
- ? &handler->args[1].child_thread_args.stop
- : NULL;
+ args->handler = handler;
args->workers_count = handler->running_count - 1;
}
@@ -129,6 +140,7 @@ split_point_t *__pool_alloc(split_point_t *pool, size_t capacity) {
atomic_store(&sp->best_score, -SEARCH_INF);
// workers counts only registered helpers; the creator registers itself
atomic_store(&sp->workers, 0);
+ atomic_store(&sp->nodes_searched, 0);
sp->best_move = (move_t) { 0 };
// recycled block: release the previous occupant's move list
moves_deinit(sp->moves);
@@ -179,6 +191,13 @@ void split_point_share(split_point_t *split_point, i32 score, move_t move) {
}
}
+// §2.1 thrashing guard: true if this split point has been searched enough
+// to be worth publishing for HELP. Prevents the end-of-iteration collapse
+// where N idle threads all try to split a single shallow node.
+bool split_point_worth_sharing(split_point_t *split_point) {
+ return atomic_load(&split_point->nodes_searched) >= SPLITPOINT_THRASH_THRESHOLD;
+}
+
split_point_t *__pool_find_work(split_point_t *pool, size_t capacity) {
for (size_t i = 0; i < capacity; i++) {
split_point_t *sp = pool + i;
@@ -194,6 +213,8 @@ split_point_t *threads_find_work(struct threads *handler) {
}
#ifdef TEST_MOD
+#include <time.h>
+
bool test_threads_args_init_root_propagates() {
engine_message_handler_t dummy[2];
engine_message_handler_t *dummy_ptrs[2] = { &dummy[0], &dummy[1] };
@@ -401,4 +422,80 @@ bool test_help_scan_finds_published_work() {
threads_deinit(&handler);
return ok;
}
+
+bool test_thrashing_counter_tracks_nodes() {
+ struct threads handler;
+ threads_init(&handler, 4);
+
+ split_point_t *sp = split_point_alloc(&handler);
+ atomic_store(&sp->nodes_searched, 0);
+
+ // initially not worth sharing
+ bool ok = true;
+ ok &= !split_point_worth_sharing(sp);
+
+ // simulate some work
+ atomic_fetch_add(&sp->nodes_searched, 100);
+ ok &= !split_point_worth_sharing(sp);
+
+ // cross the threshold
+ atomic_fetch_add(&sp->nodes_searched, 200);
+ ok &= split_point_worth_sharing(sp);
+
+ moves_deinit(sp->moves);
+ threads_deinit(&handler);
+ return ok;
+}
+
+bool test_cooperative_stop_sets_finished() {
+ struct threads handler;
+ threads_init(&handler, 2);
+ ttable_init(&handler.ttable, 2);
+
+ // set up messages
+ engine_message_handler_t msg_handlers[2];
+ engine_message_handler_t *msg_ptrs[2] = { &msg_handlers[0], &msg_handlers[1] };
+ engine_messages_t messages = { .data = msg_ptrs, .count = 2, .capacity = 2 };
+ for (int i = 0; i < 2; i++) {
+ engine_message_handler_init(&msg_handlers[i]);
+ }
+ handler.engine_messages = &messages;
+
+ go_args_t go_args = { .depth = 1 };
+ position_t pos = position_starting();
+ handler.go_args = &go_args;
+ handler.position = &pos;
+ handler.running_count = 2;
+
+ for (int i = 0; i < 2; i++) {
+ __threads_args_init(handler.args + i, &handler, i);
+ }
+
+ // start threads
+ threads_go(&handler);
+
+ // give threads time to start
+ struct timespec ts = { .tv_nsec = 10000000 }; // 10ms
+ nanosleep(&ts, NULL);
+
+ // cooperative stop
+ threads_stop_all(&handler);
+ threads_join(&handler);
+
+ bool ok = true;
+ for (int i = 0; i < 2; i++) {
+ ok &= atomic_load(&handler.args[i].root_thread_args.finished) == 1;
+ }
+
+ threads_cleanup(&handler);
+ for (int i = 0; i < 2; i++) {
+ while (engine_message_handler_can_pop(&msg_handlers[i])) {
+ engine_message_deinit(engine_message_handler_pop(&msg_handlers[i]));
+ }
+ engine_message_handler_deinit(&msg_handlers[i]);
+ }
+ ttable_deinit(handler.ttable);
+ threads_deinit(&handler);
+ return ok;
+}
#endif