Chapter 10

Search

Search is the heart of a chess engine. Evaluation tells ABC how good a position is, but search is what allows the engine to look ahead, compare possible moves, and eventually decide what it should play.

ABC begins its search with a deliberately simple evaluation function based on the PeSTO evaluation method. The evaluation uses material values and piece-square tables, with separate values for the opening and endgame. There are no advanced positional terms here. The purpose is to give the search a numerical description of the position that is simple enough to understand while still providing useful chess knowledge.

The Evaluation Function

// Position evaluation
int evaluate_position() {
    // Init params
    int game_phase = -1;
    int piece_scores = 0;
    int game_phase_score = 0;
    int score = 0;
    int score_opening = 0;
    int score_endgame = 0;
    int knights = 0;
    int bishops = 0;
    int rooks = 0;
    int queens = 0;
    
    // Evaluate material & positional scores
    for (int square = 0; square < 128; square++) {
        if (!(square & 0x88)) {
            int piece = board[square];
            score_opening += material_score[OPENING][piece];
            score_endgame += material_score[ENDGAME][piece];
			switch(piece) {
				case WK:
                    score_opening += positional_score[OPENING][KING][square];
                    score_endgame += positional_score[ENDGAME][KING][square];
                    break;
                case WP:
                    score_opening += positional_score[OPENING][PAWN][square];
                    score_endgame += positional_score[ENDGAME][PAWN][square];
                    break;
				case WN:
                    score_opening += positional_score[OPENING][KNIGHT][square];
                    score_endgame += positional_score[ENDGAME][KNIGHT][square];
                    knights++;
                    break;
				case WB:
                    score_opening += positional_score[OPENING][BISHOP][square];
                    score_endgame += positional_score[ENDGAME][BISHOP][square];
                    bishops++;
                    break;
				case WR:
                    score_opening += positional_score[OPENING][ROOK][square];
                    score_endgame += positional_score[ENDGAME][ROOK][square];
                    rooks++;
                    break;
                case WQ:
                    score_opening += positional_score[OPENING][QUEEN][square];
                    score_endgame += positional_score[ENDGAME][QUEEN][square];
                    queens++;
                    break;
                case BK:
                    score_opening -= positional_score[OPENING][KING][mirror_score[square]];
                    score_endgame -= positional_score[ENDGAME][KING][mirror_score[square]];
                    break;
                case BP:
                    score_opening -= positional_score[OPENING][PAWN][mirror_score[square]];
                    score_endgame -= positional_score[ENDGAME][PAWN][mirror_score[square]];
                    break;
				case BN:
                    score_opening -= positional_score[OPENING][KNIGHT][mirror_score[square]];
                    score_endgame -= positional_score[ENDGAME][KNIGHT][mirror_score[square]];
                    knights++;
                    break;
				case BB:
                    score_opening -= positional_score[OPENING][BISHOP][mirror_score[square]];
                    score_endgame -= positional_score[ENDGAME][BISHOP][mirror_score[square]];
                    bishops++;
                    break;
				case BR:
                    score_opening -= positional_score[OPENING][ROOK][mirror_score[square]];
                    score_endgame -= positional_score[ENDGAME][ROOK][mirror_score[square]];
                    rooks++;
                    
                    break;
                case BQ:
                    score_opening -= positional_score[OPENING][QUEEN][mirror_score[square]];
                    score_endgame -= positional_score[ENDGAME][QUEEN][mirror_score[square]];
                    queens++;
                    break;
			}
        }
    }
    
    // Calculate game phase
    game_phase_score = knights * material_score[OPENING][KNIGHT];
    game_phase_score += bishops * material_score[OPENING][BISHOP];
    game_phase_score += rooks * material_score[OPENING][ROOK];
    game_phase_score += queens * material_score[OPENING][QUEEN];
    if (game_phase_score > opening_phase_score) game_phase = OPENING;
    else if (game_phase_score < endgame_phase_score) game_phase = ENDGAME;
    else game_phase = MIDDLEGAME;
    
    // Calculate final score
    if (game_phase == MIDDLEGAME)
        score = (
            score_opening * game_phase_score +
            score_endgame * (opening_phase_score - game_phase_score)
        ) / opening_phase_score;
    else if (game_phase == OPENING) score = score_opening;
    else if (game_phase == ENDGAME) score = score_endgame;
    return (side == WHITE) ? score : -score;
}

Starting the Evaluation

The function begins by preparing several variables. The two most important scores are score_opening and score_endgame. ABC evaluates the same position twice: once using opening values and once using endgame values.

int score_opening = 0;
int score_endgame = 0;

This is the foundation of the PeSTO approach used here. A chess position should not necessarily be evaluated in exactly the same way throughout the entire game. For example, king safety is much more important in the opening, while king activity becomes increasingly valuable as pieces disappear.

Instead of trying to write a completely different evaluation function for each stage of the game, ABC calculates both scores and later blends them according to the current material on the board.

Scanning the Board

The board is traversed using the same 0x88 representation used everywhere else in ABC.

for (int square = 0; square < 128; square++) {
    if (!(square & 0x88)) {
        int piece = board[square];
        ...
    }
}

Every valid chessboard square is inspected. The first thing ABC adds is the material value of the piece on that square:

score_opening += material_score[OPENING][piece];
score_endgame += material_score[ENDGAME][piece];

The material tables already contain positive values for White pieces and negative values for Black pieces. Therefore, simply adding the value automatically produces a score from White's perspective.

The evaluation then adds a second component: the piece-square table.

Piece-Square Tables

A piece-square table assigns a positional bonus or penalty to a piece depending on where it stands. A knight in the center, for example, can be worth more than a knight trapped on the edge. The same idea can be applied differently to pawns, bishops, rooks, queens, and kings.

For White pieces, the appropriate table is indexed directly by the square:

case WN:
    score_opening += positional_score[OPENING][KNIGHT][square];
    score_endgame += positional_score[ENDGAME][KNIGHT][square];
    knights++;
    break;

ABC does the same for every White piece type. The opening and endgame tables are both accumulated at the same time.

Black pieces use the same tables, but their contribution is negative:

case BN:
    score_opening -= positional_score[OPENING][KNIGHT][mirror_score[square]];
    score_endgame -= positional_score[ENDGAME][KNIGHT][mirror_score[square]];
    knights++;
    break;

The square is passed through mirror_score because the piece-square tables are defined from White's perspective. Mirroring allows the same table to be reused for Black without maintaining a completely separate set of tables.

The result is elegant: one set of positional tables is enough for both sides. White reads the table normally, while Black reads the mirrored square and subtracts the result.

Counting the Pieces

While evaluating the board, ABC also counts knights, bishops, rooks, and queens:

int knights = 0;
int bishops = 0;
int rooks = 0;
int queens = 0;

These counters are not used to evaluate material directly. Their purpose is to determine the game phase.

Notice that pawns and kings are not counted for this calculation. The phase is based on the amount of non-pawn material remaining, because the disappearance of these pieces is a useful indication that the game is moving toward the endgame.

Determining the Game Phase

Once the board has been evaluated, ABC calculates a phase score from the remaining knights, bishops, rooks, and queens.

game_phase_score = knights * material_score[OPENING][KNIGHT];
game_phase_score += bishops * material_score[OPENING][BISHOP];
game_phase_score += rooks * material_score[OPENING][ROOK];
game_phase_score += queens * material_score[OPENING][QUEEN];

The result is compared with two predefined limits:

if (game_phase_score > opening_phase_score) game_phase = OPENING;
else if (game_phase_score < endgame_phase_score) game_phase = ENDGAME;
else game_phase = MIDDLEGAME;

This gives ABC three possible phases: OPENING, MIDDLEGAME, and ENDGAME.

When many pieces are still present, the phase score is high and the position is treated as an opening. When most of the major and minor pieces have disappeared, the score becomes low and the position is treated as an endgame. Positions between those limits belong to the middlegame.

Blending Opening and Endgame Evaluation

The most interesting case is the middlegame. ABC does not abruptly switch from one evaluation to another. Instead, it smoothly blends the opening and endgame scores.

if (game_phase == MIDDLEGAME)
    score = (
        score_opening * game_phase_score +
        score_endgame * (opening_phase_score - game_phase_score)
    ) / opening_phase_score;

When the phase score is closer to the opening threshold, the opening evaluation has more influence. As pieces disappear and the phase score falls, the endgame evaluation gradually receives more weight.

This is an important detail of the PeSTO method. The engine does not need to decide that one position is suddenly an opening and the next one is suddenly an endgame. Instead, the evaluation smoothly changes as the material disappears.

At the two extremes, no blending is necessary:

else if (game_phase == OPENING) score = score_opening;
else if (game_phase == ENDGAME) score = score_endgame;

So an opening position uses the opening score directly, an endgame position uses the endgame score directly, and a middlegame position receives a weighted combination of both.

Evaluating from the Side to Move

Finally, ABC adjusts the result according to whose turn it is.

return (side == WHITE) ? score : -score;

Until this point, the score has been calculated from White's perspective: positive means the position favors White and negative means it favors Black.

The search, however, works from the perspective of the side that is currently moving. If it is White's turn, the score is returned unchanged. If it is Black's turn, the score is negated.

This small detail will become extremely important when we reach negamax. It allows the search to use the same logic for both players: every side simply tries to maximize its own score.

A Simple Evaluation by Design

There is a lot that ABC could add to this evaluation: mobility, pawn structure, king safety, passed pawns, bishop pair, rook activity, threats, and many other positional concepts. It deliberately does not do that here.

The evaluation is intentionally built around material and PeSTO piece-square tables. This keeps the method understandable while giving the search a meaningful foundation to work with. The real strength of the engine will come from what happens next: searching through the possible future positions and using this evaluation at the leaves of that search.

Move Ordering

Once evaluation can tell ABC whether a position is good or bad, the next challenge is deciding which moves to search first. This matters enormously in alpha-beta search: finding strong moves early allows the search to cut off large parts of the move tree.

ABC uses a small set of heuristics to give every move an urgency score. It also needs a way to recognize repeated positions, which is where the position hashing system from the previous chapter comes into play.

Scoring Moves

// Score urgency for move ordering
int score_move(int move) {
    if (pv_table[0][ply] == move) return 20000;
    int score = mvv_lva[board[get_move_source(move)]][board[get_move_target(move)]];         
    if (get_move_capture(move)) score += 10000;
    else {
        if (killer_moves[0][ply] == move) score = 9000;
        else if (killer_moves[1][ply] == move) score = 8000;
        else score = history_moves[board[get_move_source(move)]][get_move_target(move)] + 7000;
    } return score;
}

The function assigns a numerical score to a move. A higher score means that ABC considers the move more urgent and therefore wants to search it earlier.

The first check gives the highest possible priority to the current principal variation move:

if (pv_table[0][ply] == move) return 20000;

If the move matches the move stored in the principal variation at the current search ply, ABC immediately gives it a score of 20000. The PV represents the line that the search currently considers best, so trying that move first is usually a very good bet.

Captures and MVV-LVA

For other moves, ABC starts with the MVV-LVA score:

int score = mvv_lva[board[get_move_source(move)]][board[get_move_target(move)]];

MVV-LVA stands for Most Valuable Victim - Least Valuable Attacker. It gives captures a priority based on the value of the captured piece and the value of the attacking piece.

Captures then receive an additional bonus:

if (get_move_capture(move)) score += 10000;

This ensures that captures are normally searched before ordinary quiet moves. The exact MVV-LVA value then determines the ordering between captures.

Killer Moves

Quiet moves are handled differently. If a move is not a capture, ABC checks whether it is one of the two killer moves stored for the current search ply.

if (killer_moves[0][ply] == move) score = 9000;
else if (killer_moves[1][ply] == move) score = 8000;

A killer move is a quiet move that previously caused a strong alpha-beta cutoff at the same depth of the search. The idea is simple: if a quiet move was powerful enough to cause a cutoff before, it is worth trying early again.

ABC keeps two killer moves for each ply and gives the first one a higher priority than the second.

History Heuristic

If the move is neither a capture nor a killer move, ABC falls back to the history heuristic:

else score = history_moves[board[get_move_source(move)]][get_move_target(move)] + 7000;

The history table records how successful particular quiet moves have been during previous searches. Moves with a stronger history score are therefore searched earlier.

The 7000 offset places ordinary history moves below killer moves while still keeping them above the base range of ordinary move scores.

The Priority Hierarchy

Putting the rules together, ABC effectively establishes this ordering:

Principal variation move      20000
Captures                      10000 + MVV-LVA
First killer move             9000
Second killer move            8000
History moves                 7000 + history score

This hierarchy gives the search a strong preference for moves that are likely to produce useful cutoffs. The exact values are not chess knowledge by themselves; they are simply arranged so that the different heuristics can be compared numerically.

Sorting the Move List

Once every move has a score, ABC sorts the move list from highest urgency to lowest.

// Sort moves based on urgency
void sort_moves(Movelist *moves) {
    int move_scores[moves->count];
    for (int count = 0; count < moves->count; count++)
        move_scores[count] = score_move(moves->moves[count]);
    for (int current = 0; current < moves->count; current++) {
        for (int next = current + 1; next < moves->count; next++) {
            if (move_scores[current] < move_scores[next]) {
                int temp_score = move_scores[current];
                move_scores[current] = move_scores[next];
                move_scores[next] = temp_score;
                int temp_move = moves->moves[current];
                moves->moves[current] = moves->moves[next];
                moves->moves[next] = temp_move;
            }
        }
    }    
}

ABC first creates a temporary array containing the score of every move. It then compares the scores and swaps both the scores and their corresponding moves whenever a later move has a higher priority.

The result is a move list ordered from the most promising move to the least promising move.

The sorting algorithm itself is intentionally simple. A stronger implementation could use a more efficient sorting method or avoid fully sorting the list altogether. But for ABC, this straightforward implementation makes the relationship between move scores and move order easy to see.

Why Move Ordering Matters

Move ordering does not change the correctness of alpha-beta search. In principle, the engine can search the moves in any order and still reach the same result. What changes dramatically is how much of the tree must be searched.

If a strong move is searched early, alpha-beta can often prove that many other moves are unnecessary. If the best move is searched last, the engine may have to examine far more of the tree before reaching the same conclusion.

That is why these small heuristics have such a large effect on practical search performance.

Repetition Detection

Another helper handles a completely different search problem: detecting whether the current position has occurred before.

// Position repetition detection
int is_repetition() {
    for (int index = 0; index < repetition_index; index++)
        if (repetition_table[index] == generate_hash_key()) return 1;
    return 0;
}

ABC already stores previously encountered position hashes in repetition_table. To determine whether the current position is a repetition, the function generates the current position's hash and compares it against every stored hash.

if (repetition_table[index] == generate_hash_key()) return 1;

If a matching hash is found, the function immediately returns 1. If the entire table is checked without finding a match, the function returns 0.

This is where the position hashing code from the previous chapter becomes useful. Rather than comparing the entire board and its state against every previous position, ABC compares compact integer keys.

NOTE: Recalculating the hash from scratch is inefficient but straightforward and less error-prone; an optimized approach updates the hash incrementally with each move, trading simplicity for additional complexity.

These helpers prepare the search for what comes next. sort_moves() helps alpha-beta search the most promising moves first, while is_repetition() helps the search recognize drawn positions. Together with the evaluation function, they provide the basic tools ABC needs to begin actually searching the game tree.

Quiescence Search

A normal search stops at a fixed depth, but the position at that point may still be tactically unstable. Quiescence search extends the search selectively so that ABC does not evaluate a position in the middle of an unresolved capture sequence.

Listening for Stop

Quiescence search can visit a very large number of positions, so it must continue listening for the UCI stop command. Just like the main search, ABC calls communicate() periodically rather than at every node. The nodes & 2047 check keeps this overhead small.

// Listen to UCI "stop" command
if ((nodes & 2047 ) == 0) communicate();

// Count nodes
nodes++;

Static Evaluation

The first step is to evaluate the current position without making another move. This gives quiescence search a starting score and allows the usual alpha-beta bounds to be applied immediately.

// Static evaluation
int eval = evaluate_position();
if (eval >= beta) return beta;
if (eval > alpha) alpha = eval;

If the static evaluation already reaches beta, the position is good enough to cause a cutoff and there is no reason to search further. Otherwise, if it improves alpha, the current position becomes the best result found so far.

Searching Only Captures

ABC then generates moves and orders them using the same sort_moves() used by the main search. However, the actual move-making call uses ONLY_CAPTURES. This means quiescence search does not continue playing ordinary quiet moves; it concentrates only on tactical captures that could change the evaluation.

// Generate moves
Movelist moves[1];
generate_moves(moves);

// Move ordering
sort_moves(moves);
// Search best move
for (int count = 0; count < moves->count; count++) {      
    Position position;
    save_position(&position); ply++;
    if (!make_move(moves->moves[count], ONLY_CAPTURES)) { ply--; continue; }
    int score = -quiescence_search(-beta, -alpha);
    restore_position(&position); ply--;
    if (stopped == 1) break;
    if (score > alpha) {
        alpha = score;
        if (score >= beta) return beta;
    }
} return alpha;

Each candidate capture is played, and quiescence search calls itself from the opponent's point of view using the negated alpha-beta window. The position is then restored before trying the next move. The same save_position() and restore_position() mechanism used by the main search makes this recursive process straightforward.

If the recursive score improves alpha, ABC keeps it. If it reaches beta, the branch is cut off immediately. If the search was stopped, the loop terminates without continuing to explore more captures.

Why quiescence search matters

Without quiescence search, the main search could stop just before a capture and evaluate a position that is about to change dramatically. By continuing through tactical captures until the position becomes quieter, ABC gets a more stable evaluation at the search horizon.

Negamax Search

Negamax is the core of ABC's chess search. It recursively explores the game tree, evaluates positions from the current side's perspective, detects tactical and terminal positions, and uses alpha-beta pruning to discard branches that cannot improve the result.

Preparing the Search

// Init params
int legal_moves = 0;
int old_alpha = alpha;
pv_length[ply] = ply;

legal_moves tracks whether the position contains at least one legal move, which is important later for distinguishing checkmate from stalemate. old_alpha stores the original alpha value for the search node. The principal variation length is initialized to the current ply, preparing the PV table to be updated if a better move is found.

Threefold Repetition

// 3 fold repetition detection
if (ply && is_repetition()) return 0;

Before searching deeper, ABC checks for repetition. If the current position has already appeared in the repetition table, the position is treated as a draw and the search returns 0. The ply check prevents the root position from being treated as an immediate repetition before any moves have been played.

Stopping the Search

// Listen to UCI "stop" command
if ((nodes & 2047 ) == 0) communicate();

Search can become extremely large, so ABC periodically calls communicate() to process UCI commands such as stop. Checking every 2048 nodes keeps the search responsive without adding significant overhead to every node.

Reaching the Quiescence Search

// Search until no captures left
if (!depth) return quiescence_search(alpha, beta);

When the requested search depth reaches zero, ABC does not immediately evaluate the position. Instead, it enters quiescence_search(), which continues through tactical captures until the position is sufficiently quiet. This prevents the horizon of the main search from ending in the middle of an important tactical sequence.

Counting Nodes and Extending Check

// Count nodes
nodes++;

// Search deeper if in check
int in_check = is_square_attacked(king_square[side], side ^ 1);
if (in_check) depth++;

Every normal search node is counted in nodes. ABC then checks whether the side to move is currently in check. If it is, the search depth is extended by one ply. This is a simple form of check extension: positions where the king is under immediate threat deserve additional search because stopping there could produce an unstable evaluation.

Generating and Ordering Moves

// Generate moves
Movelist moves[1];
generate_moves(moves);

// Move ordering
sort_moves(moves);

ABC generates the available moves and immediately orders them. This is crucial for alpha-beta search: when strong moves are examined first, the search is much more likely to discover cutoffs early and avoid exploring unnecessary branches.

Recursive Negamax

// Search best move
for (int count = 0; count < moves->count; count++) {
    int move = moves->moves[count];
    Position position;
    save_position(&position); ply++;
    repetition_index++;
    repetition_table[repetition_index] = generate_hash_key();
    if (!make_move(move, ALL_MOVES)) { ply--; repetition_index--; continue; }
    legal_moves++; int score = 0;
    score = -negamax_search(-beta, -alpha, depth - 1);
    restore_position(&position); ply--; repetition_index--;
    if (stopped == 1) break;
    if (score > alpha) {
        history_moves[board[get_move_source(move)]][get_move_target(move)] += depth;
        alpha = score;
        pv_table[ply][ply] = move;
        for (int i = ply + 1; i < pv_length[ply + 1]; i++) pv_table[ply][i] = pv_table[ply + 1][i];
        pv_length[ply] = pv_length[ply + 1];
        if (score >= beta) {
            killer_moves[1][ply] = killer_moves[0][ply];
            killer_moves[0][ply] = move;
            return beta;
        }
    }
}

Each move is searched from the opponent's perspective. ABC first saves the position, advances the search ply, records the resulting position for repetition detection, and attempts to make the move. Illegal moves are discarded by make_move().

The recursive call is the essence of negamax:

score = -negamax_search(-beta, -alpha, depth - 1);

Instead of maintaining separate maximizing and minimizing code, negamax relies on the fact that one player's advantage is the other player's disadvantage. The recursive search therefore receives the negated alpha-beta window, and its result is negated when it returns.

Updating the Best Move

if (score > alpha) {
    history_moves[board[get_move_source(move)]][get_move_target(move)] += depth;
    alpha = score;
    pv_table[ply][ply] = move;
    for (int i = ply + 1; i < pv_length[ply + 1]; i++)
        pv_table[ply][i] = pv_table[ply + 1][i];
    pv_length[ply] = pv_length[ply + 1];

When a move produces a score better than the current alpha bound, ABC has found a new best move at this node. The history heuristic is rewarded according to the current search depth, helping the move become more attractive in future move ordering.

The principal variation is also updated. The current move is placed at the beginning of the PV, followed by the continuation found by the child search. In this way, the PV table gradually describes the best line discovered through the recursive search.

Alpha-Beta Cutoff

if (score >= beta) {
    killer_moves[1][ply] = killer_moves[0][ply];
    killer_moves[0][ply] = move;
    return beta;
}

If the score reaches or exceeds beta, the remaining moves do not need to be searched. The current move has proven strong enough to cause an alpha-beta cutoff, so ABC immediately returns beta.

This move is also stored as the first killer move for the current ply, while the previous first killer becomes the second. Killer moves are particularly useful because a quiet move that caused a cutoff here may be a strong candidate for causing a cutoff again at the same depth elsewhere in the tree.

Checkmate and Stalemate

// Checkmate / Stalemate detection
if (!legal_moves) {
    if (in_check) return -49000 + ply;
    else return 0;
} return alpha;

After all generated moves have been examined, ABC checks whether any legal move existed. If there were none and the king is in check, the position is checkmate and a large negative score is returned. The ply adjustment makes a mate found sooner preferable to an equally scored mate found deeper in the tree.

If there are no legal moves but the king is not in check, the position is stalemate and the result is a draw, represented by 0. Otherwise, the best score found during the search is simply returned as alpha.

The heart of ABC

This single recursive function brings together many of the systems built so far: move generation, legality checking, position restoration, repetition detection, quiescence search, move ordering, the history and killer heuristics, principal variation tracking, and alpha-beta pruning. The search itself is deliberately understandable; each optimization is added around the basic recursive idea rather than hiding it.

Iterative Deepening and UCI Output

The final piece of ABC's search is the function that connects the search algorithm to the outside world. Called directly by the UCI interface, search_position() prepares a new search, repeatedly deepens it, reports its progress, and finally returns the best move found in the principal variation.

Starting a New Search

// search position
int search_position(int depth) {
    int start = get_time_ms();
    // Clear search
    nodes = 0;
    stopped = 0;
    ply = 0;
    memset(pv_table, 0, sizeof(pv_table));
    memset(pv_length, 0, sizeof(pv_length));
    memset(killer_moves, 0, sizeof(killer_moves));
    memset(history_moves, 0, sizeof(history_moves));

Every search begins with a clean search state. ABC records the start time, resets the node counter and stop flag, and returns the search ply to zero. The principal variation, killer moves, and history heuristic are also cleared so that the new search starts without stale information from the previous position.

Iterative Deepening

// Iterative deepening
for (int current_depth = 1; current_depth <= depth; current_depth++) {
    // Search position with current depth
    int score = negamax_search(-50000, 50000, current_depth);
    if (stopped == 1) break;

ABC does not immediately search to the requested depth. Instead, it uses iterative deepening: first depth 1, then depth 2, then depth 3, and so on until the requested depth is reached or the search is stopped.

This gives the engine a useful result even when it cannot finish the deepest search. Every completed iteration has produced a complete principal variation, while deeper iterations can use the information from previous iterations for move ordering.

The search uses the full numerical window from -50000 to 50000. These values represent the practical search limits of ABC, with mate scores handled separately around ±49000.

Reporting Search Information

// Output UCI info
if (score > -49000 && score < -48000)
    printf("info score mate %d depth %d nodes %lld time %d pv ", -(score + 49000) / 2 - 1, current_depth, nodes, get_time_ms() - start);
else if (score > 48000 && score < 49000)
    printf("info score mate %d depth %d nodes %lld time %d pv ", (49000 - score) / 2 + 1, current_depth, nodes, get_time_ms() - start);
else printf("info score cp %d depth %d nodes %lld time %d pv ", score, current_depth, nodes, get_time_ms() - start);

After every completed iteration, ABC reports the result using the UCI info format. A normal evaluation is reported as centipawns with score cp. Scores close to the mate range are converted into a mate distance and reported with score mate.

The engine also reports the completed search depth, number of nodes searched, elapsed time, and principal variation. This information allows a UCI chess interface to display what the engine is currently thinking about.

Printing the Principal Variation

for (int i = 0; i < pv_length[0]; i++) {
    int move = pv_table[0][i];
    print_move(get_move_source(move), get_move_target(move), get_move_promoted(move));
} printf("\n"); fflush(stdout);

The principal variation stored at the root is converted back into UCI move notation using print_move(). The PV is the line of moves that the search currently considers best, so it gives the GUI a human-readable view of the engine's calculation.

fflush(stdout) is important here because UCI communication is interactive. ABC immediately sends its output to the GUI instead of allowing it to remain buffered.

Returning the Best Move

// print best move
int move = pv_table[0][0];
printf("\nbestmove ");
print_move(get_move_source(move), get_move_target(move), get_move_promoted(move));
printf("\n");

Once iterative deepening finishes, the first move of the root principal variation is the engine's chosen move. ABC prints it using the UCI bestmove command, completing the communication required by the GUI.

The Complete Search Pipeline

With this function, the pieces of ABC's search finally come together. The UCI interface calls search_position(), which repeatedly invokes negamax_search(). Negamax uses alpha-beta pruning, move ordering, killer and history heuristics, repetition detection, check extensions, and quiescence search. The evaluation function provides the numerical judgment of each position, while the principal variation records the best line found.

The result is a deliberately simple but complete chess-engine search pipeline: UCI command → iterative deepening → negamax → quiescence → evaluation → principal variation → best move.

Why iterative deepening is so useful

Iterative deepening may appear wasteful because the engine searches the same position at several depths, but the earlier searches are valuable. They establish a strong principal variation and useful move-ordering information before the next, more expensive iteration begins. More importantly for ABC, if the search is stopped during a deeper iteration, the last completed iteration has already produced a valid result.