diff options
Diffstat (limited to 'src/engine/search.c')
| -rw-r--r-- | src/engine/search.c | 160 |
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 |
