summaryrefslogtreecommitdiff
path: root/src/engine/search.c
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/search.c
parentd87b4744bed8f0c43e11ea12508ef2941bdd7186 (diff)
bug fixes
Diffstat (limited to 'src/engine/search.c')
-rw-r--r--src/engine/search.c160
1 files changed, 159 insertions, 1 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