Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions see_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,36 @@ for c in "${CASES[@]}"; do
fi
done
done

# Quiet moves must also traverse the exchange sequence. Qa1-a2 walks onto an
# undefended rook attack and therefore has SEE -900; Kh1-g1 is safe (SEE 0).
QUIET_CASES=(
"r6k/8/8/8/8/8/8/Q6K w - - 0 1|a1a2|-900"
"7k/8/8/8/8/8/8/7K w - - 0 1|h1g1|0"
"7k/8/8/8/4p3/8/6P1/6NK w - - 0 1|g1f3|-220"
"4k3/8/8/8/8/8/8/4K2R w K - 0 1|e1g1|0"
"4r2k/8/8/8/8/8/8/4K3 w - - 0 1|e1e2|-20000"
)
for c in "${QUIET_CASES[@]}"; do
fen="${c%%|*}"; rest="${c#*|}"; mv="${rest%%|*}"; exp="${rest##*|}"
see_out=$(printf 'position fen %s\nseedump\nquit\n' "$fen" | $ENG 2>/dev/null | grep "^see $mv ")
got_see=$(echo "$see_out" | sed 's/.*= //')
if [ "$got_see" = "$exp" ]; then
pass=$((pass+1))
else
fail=$((fail+1)); echo "FAIL quiet SEE $mv : erwartet $exp, bekommen '${got_see:-<kein Ergebnis>}' [$fen]"
fi
for delta in -1 0 1; do
threshold=$((exp + delta))
if [ "$delta" -le 0 ]; then expected_ge=1; else expected_ge=0; fi
ge_out=$(printf 'position fen %s\nseege %s %d\nquit\n' "$fen" "$mv" "$threshold" | $ENG 2>/dev/null | grep "^seege $mv $threshold ")
got_ge=$(echo "$ge_out" | sed 's/.*= //')
if [ "$got_ge" = "$expected_ge" ]; then
pass=$((pass+1))
else
fail=$((fail+1)); echo "FAIL quiet see_ge $mv threshold $threshold : erwartet $expected_ge, bekommen '${got_ge:-<kein Ergebnis>}' [$fen]"
fi
done
done
echo "----"
echo "PASS=$pass FAIL=$fail"
110 changes: 62 additions & 48 deletions src/search.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ static struct {
uint64_t singular_hits;
uint64_t singular_extensions;
uint64_t singular_multicuts;
uint64_t main_quiet_see;
} pruning_stats;

#define TT_STAT_INC(var) ((var)++)
Expand Down Expand Up @@ -105,6 +106,36 @@ static void init_lmr_table(void) {
lmr_table_initialized = true;
}

static int quiet_lmr_reduction(const SearchInfo* info, int depth, int moves_searched,
bool is_pv, bool in_check, int move_score) {
if (!info->params.use_lmr || in_check ||
depth < info->params.lmr_reduction_limit ||
moves_searched < info->params.lmr_full_depth_moves) {
return 0;
}

int lmr_depth = depth < MAX_PLY ? depth : MAX_PLY - 1;
int lmr_moves = moves_searched < 64 ? moves_searched : 63;
int reduction = lmr_table[lmr_depth][lmr_moves];

if (!is_pv) reduction++;

if (move_score < info->params.lmr_stat_low2) {
reduction += 2;
} else if (move_score < info->params.lmr_stat_low1) {
reduction++;
} else if (move_score > info->params.lmr_stat_high2) {
reduction -= 2;
} else if (move_score > info->params.lmr_stat_high1) {
reduction--;
}

if (reduction < 1) reduction = 1;
if (depth - 1 - reduction < 1) reduction = depth - 2;
if (reduction < 0) reduction = 0;
return reduction;
}

void set_search_silent(bool silent) {
search_silent_mode = silent;
}
Expand Down Expand Up @@ -139,13 +170,16 @@ void search_params_init(SearchParams* params) {
params->use_iir = true;
params->use_probcut = true;
params->use_singular = true;
params->use_main_quiet_see = true;

// Late Move Pruning: skip quiets after base + depth^2 searched moves
params->lmp_base = 6;
params->lmp_max_depth = 8;

params->main_capture_see_margin = 100;
params->main_capture_see_max_depth = 16;
params->main_quiet_see_margin = 20;
params->main_quiet_see_max_depth = 8;

// Internal Iterative Reduction: reduce one ply when move ordering has no
// transposition-table move to guide a sufficiently deep non-root node.
Expand Down Expand Up @@ -391,11 +425,6 @@ static int see(const Board* board, Move move) {
victim_value = SEE_VALUES[1]; // Pawn
}

// If capturing nothing (and not promoting), there is no exchange to evaluate
if (victim_value == 0 && !MOVE_IS_EN_PASSANT(move) && !MOVE_IS_PROMOTION(move)) {
return 0;
}

// Gain array to track material balance at each step
int gain[32];
int depth = 0;
Expand Down Expand Up @@ -491,9 +520,6 @@ static bool see_ge(const Board* board, Move move, int threshold) {
int victim_value = get_piece_value(victim_type);
if (MOVE_IS_EN_PASSANT(move)) victim_value = SEE_VALUES[1];

if (victim_value == 0 && !MOVE_IS_EN_PASSANT(move) && !MOVE_IS_PROMOTION(move))
return 0 >= threshold;

int64_t balance = (int64_t)victim_value + promo_gain - threshold;
if (balance < 0) return false;

Expand Down Expand Up @@ -1482,6 +1508,12 @@ static int negamax(Board* board, int depth, int alpha, int beta, SearchInfo* inf
bool can_main_capture_see = info->params.use_main_capture_see && ply > 0 &&
!is_pv && !in_check && depth <= info->params.main_capture_see_max_depth &&
abs(alpha) < TB_SCORE_MIN && abs(beta) < TB_SCORE_MIN;
// Quiet SEE pruning is independent from capture SEE pruning. Its threshold
// uses the move's history-adjusted LMR depth, so well-ordered moves receive
// a wider allowance than late moves that are already heavily reduced.
bool can_main_quiet_see = info->params.use_main_quiet_see && ply > 0 &&
!is_pv && !in_check && depth <= info->params.main_quiet_see_max_depth &&
abs(alpha) < TB_SCORE_MIN && abs(beta) < TB_SCORE_MIN;
// Static eval at every eligible non-check node, reusing the TT copy when
// available. This also feeds the per-ply stack used by improving-aware LMP.
int raw_static_eval = TT_EVAL_NONE;
Expand Down Expand Up @@ -1712,6 +1744,7 @@ static int negamax(Board* board, int depth, int alpha, int beta, SearchInfo* inf
bool is_capture = MOVE_IS_CAPTURE(m);
bool is_promotion = MOVE_IS_PROMOTION(m);
bool is_tactical = is_capture || is_promotion;
int reduction = 0;

if (!is_tactical && quiets_tried_count < MAX_MOVES) {
quiets_tried[quiets_tried_count++] = m;
Expand Down Expand Up @@ -1750,6 +1783,24 @@ static int negamax(Board* board, int depth, int alpha, int beta, SearchInfo* inf
continue;
}

if (!is_tactical) {
reduction = quiet_lmr_reduction(info, depth, moves_searched,
is_pv, in_check, move_score);
}
int lmr_search_depth = depth - 1 - reduction;

// Main-search quiet SEE pruning. Preserve the TT move, the first
// well-ordered candidates, and quiets with non-negative history so a
// cut node does not lose its likely cutoff move before SEE is considered.
if (can_main_quiet_see && !is_tactical && m != mp.tt_move &&
moves_searched >= info->params.lmr_full_depth_moves &&
move_score < 0 &&
!see_ge(board, m, -info->params.main_quiet_see_margin *
lmr_search_depth * lmr_search_depth)) {
PRUNING_STAT_INC(main_quiet_see);
continue;
}

// Prefetch TT entry for likely next position
// (This is a simple optimization that can help cache performance)

Expand Down Expand Up @@ -1836,45 +1887,6 @@ static int negamax(Board* board, int depth, int alpha, int beta, SearchInfo* inf
info, ply + 1, true, false, 0);
} else {
// Late Move Reductions - logarithmic formula like Stockfish
int reduction = 0;

if (info->params.use_lmr && !in_check && !is_tactical &&
depth >= info->params.lmr_reduction_limit &&
moves_searched >= info->params.lmr_full_depth_moves) {

// Lookup from precomputed table (Stockfish-style formula)
int lmr_depth = depth < MAX_PLY ? depth : MAX_PLY - 1;
int lmr_moves = moves_searched < 64 ? moves_searched : 63;
reduction = lmr_table[lmr_depth][lmr_moves];

// Increase reduction for non-PV nodes
if (!is_pv) {
reduction++;
}

// Adjust based on the combined history score (butterfly +
// continuation history = this quiet's ordering score, computed
// before applyMove). Thresholds match the SF-scale update
// magnitudes in history_bonus()/history_malus().
int stat = move_score;
if (stat < info->params.lmr_stat_low2) {
reduction += 2;
} else if (stat < info->params.lmr_stat_low1) {
reduction++;
} else if (stat > info->params.lmr_stat_high2) {
reduction -= 2;
} else if (stat > info->params.lmr_stat_high1) {
reduction--;
}

// Clamp reduction: at least 1, don't reduce below depth 1
if (reduction < 1) reduction = 1;
if (depth - 1 - reduction < 1) {
reduction = depth - 2;
}
if (reduction < 0) reduction = 0;
}

// Null window search with possible reduction
if (reduction > 0) {
PRUNING_STAT_INC(lmr);
Expand Down Expand Up @@ -2330,7 +2342,7 @@ Move iterative_deepening_search(Board* board, SearchInfo* info) {
pruning_stats.lmp + pruning_stats.lmr +
pruning_stats.delta + pruning_stats.see_pruning +
pruning_stats.probcut_cuts;
total_prunings += pruning_stats.main_capture_see;
total_prunings += pruning_stats.main_capture_see + pruning_stats.main_quiet_see;
total_prunings += pruning_stats.iir;
printf("info string Pruning stats (total=%llu):\n", (unsigned long long)total_prunings);
printf("info string Null Move: %llu\n", (unsigned long long)pruning_stats.null_move);
Expand All @@ -2353,6 +2365,8 @@ Move iterative_deepening_search(Board* board, SearchInfo* info) {
(unsigned long long)pruning_stats.probcut_cuts,
(unsigned long long)pruning_stats.probcut_nodes);
printf("info string Main Cap SEE: %llu\n", (unsigned long long)pruning_stats.main_capture_see);
printf("info string Main Quiet: %llu\n",
(unsigned long long)pruning_stats.main_quiet_see);
printf("info string IIR: %llu\n", (unsigned long long)pruning_stats.iir);
int cmin = 0, cmax = 0, nonzero = 0, saturated = 0;
for (int s = 0; s < 2; s++)
Expand Down
6 changes: 5 additions & 1 deletion src/search.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ typedef struct {
bool use_iir; // Enable Internal Iterative Reduction
bool use_probcut; // Enable capture-only ProbCut
bool use_singular; // Enable singular extension verification
bool use_main_quiet_see; // Enable main-search SEE pruning for quiet moves

// Late Move Reduction parameters
int lmr_full_depth_moves; // Number of moves before LMR kicks in (default: 4)
Expand All @@ -55,6 +56,10 @@ typedef struct {
int main_capture_see_margin; // Negative threshold per depth (default: 100)
int main_capture_see_max_depth; // Maximum depth (default: 16)

// Main-search quiet SEE pruning (quadratic reduced-depth threshold)
int main_quiet_see_margin; // Negative threshold per LMR depth squared (default: 20)
int main_quiet_see_max_depth; // Maximum depth (default: 8)

// Internal Iterative Reduction
int iir_min_depth; // Minimum depth at a non-root node without a TT move

Expand All @@ -67,7 +72,6 @@ typedef struct {
int singular_min_depth; // Minimum depth for verification (default: 8)
int singular_margin; // Singular beta margin per depth (default: 2)
int singular_tt_depth_slack;// Allowed TT depth deficit (default: 3)

// Null Move Pruning parameters (reduction itself is adaptive in negamax)
int null_move_min_depth; // Minimum depth for null move pruning (default: 3)

Expand Down
36 changes: 24 additions & 12 deletions src/uci.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ void uci_loop() {
printf("option name Use_IIR type check default true\n");
printf("option name Use_ProbCut type check default true\n");
printf("option name Use_Singular type check default true\n");
printf("option name Use_MainQuietSEE type check default true\n");
// Search parameter options
printf("option name LMR_FullDepthMoves type spin default 3 min 1 max 10\n");
printf("option name LMP_Base type spin default 6 min 1 max 20\n");
Expand All @@ -278,6 +279,8 @@ void uci_loop() {
printf("option name Singular_MinDepth type spin default 8 min 4 max 32\n");
printf("option name Singular_Margin type spin default 2 min 0 max 16\n");
printf("option name Singular_TTDepthSlack type spin default 3 min 0 max 16\n");
printf("option name MainQuietSEE_Margin type spin default 20 min 0 max 500\n");
printf("option name MainQuietSEE_MaxDepth type spin default 8 min 1 max 16\n");
printf("option name LMR_ReductionLimit type spin default 1 min 1 max 6\n");
printf("option name NullMove_MinDepth type spin default 4 min 1 max 6\n");
printf("option name Futility_Margin type spin default 243 min 50 max 400\n");
Expand Down Expand Up @@ -311,29 +314,29 @@ void uci_loop() {
printf("readyok\n");
fflush(stdout);
} else if (strncmp(line, "seedump", 7) == 0) {
// Debug: print SEE for every capture/promotion in current position
MoveList caps;
generateCaptureAndPromotionMoves(&current_board, &caps);
for (int i = 0; i < caps.count; i++) {
// Debug: print SEE for every generated move in current position.
MoveList moves;
generateMoves(&current_board, &moves);
for (int i = 0; i < moves.count; i++) {
char s[6];
moveToString(caps.moves[i], s);
printf("see %s = %d\n", s, see_debug(&current_board, caps.moves[i]));
moveToString(moves.moves[i], s);
printf("see %s = %d\n", s, see_debug(&current_board, moves.moves[i]));
}
fflush(stdout);
} else if (strncmp(line, "seege ", 6) == 0) {
// Debug: evaluate one generated capture/promotion against a threshold.
// Debug: evaluate one generated move against a threshold.
char requested[6] = {0};
int threshold;
if (sscanf(line + 6, "%5s %d", requested, &threshold) == 2) {
MoveList caps;
generateCaptureAndPromotionMoves(&current_board, &caps);
MoveList moves;
generateMoves(&current_board, &moves);
bool found = false;
for (int i = 0; i < caps.count; i++) {
for (int i = 0; i < moves.count; i++) {
char move_string[6];
moveToString(caps.moves[i], move_string);
moveToString(moves.moves[i], move_string);
if (strcmp(move_string, requested) == 0) {
printf("seege %s %d = %d\n", requested, threshold,
see_ge_debug(&current_board, caps.moves[i], threshold));
see_ge_debug(&current_board, moves.moves[i], threshold));
found = true;
break;
}
Expand Down Expand Up @@ -414,6 +417,9 @@ void uci_loop() {
} else if (strcmp(option_name, "Use_Singular") == 0) {
search_params.use_singular = bool_value;
printf("info string Set Use_Singular to %s\n", bool_value ? "true" : "false");
} else if (strcmp(option_name, "Use_MainQuietSEE") == 0) {
search_params.use_main_quiet_see = bool_value;
printf("info string Set Use_MainQuietSEE to %s\n", bool_value ? "true" : "false");
// Numeric parameters
} else if (strcmp(option_name, "LMP_Base") == 0) {
search_params.lmp_base = value;
Expand Down Expand Up @@ -448,6 +454,12 @@ void uci_loop() {
} else if (strcmp(option_name, "Singular_TTDepthSlack") == 0) {
search_params.singular_tt_depth_slack = value;
printf("info string Set Singular_TTDepthSlack to %d\n", value);
} else if (strcmp(option_name, "MainQuietSEE_Margin") == 0) {
search_params.main_quiet_see_margin = value;
printf("info string Set MainQuietSEE_Margin to %d\n", value);
} else if (strcmp(option_name, "MainQuietSEE_MaxDepth") == 0) {
search_params.main_quiet_see_max_depth = value;
printf("info string Set MainQuietSEE_MaxDepth to %d\n", value);
} else if (strcmp(option_name, "LMR_FullDepthMoves") == 0) {
search_params.lmr_full_depth_moves = value;
printf("info string Set LMR_FullDepthMoves to %d\n", value);
Expand Down
Loading