Move Generation
Encoding and Adding Moves
Before ABC can generate moves, it needs a compact way to represent them. Every move contains several pieces of information: where the piece starts, where it goes, whether it promotes, and whether it has any special properties.
ABC packs all of this information into a single integer:
int encode_move(int source, int target, int promoted, int capture, int push, int enpassant, int castling) {
return (
(source) |
(target << 7) |
(promoted << 14) |
(capture << 18) |
(push << 19) |
(enpassant << 20) |
(castling << 21)
);
}
The move is divided into several bit fields. Each field has a fixed position inside the integer:
bits 0 - 6 source square
bits 7 - 13 target square
bits 14 - 17 promoted piece
bit 18 capture flag
bit 19 double pawn push flag
bit 20 en passant flag
bit 21 castling flag
bits 22 - 31 unused
The source and target squares each use seven bits because ABC represents squares using the 0x88 board, where every square is stored as an index from 0 to 127. The promoted piece uses four bits, while each special property is represented by a single flag bit.
The bitwise OR operator combines all of these fields into one integer. The shift operators move each value into its assigned position. For example, the target square is shifted seven bits because the first seven bits are already occupied by the source square.
Extracting Move Information
Once a move has been encoded, ABC needs to recover its individual components. The accessor functions reverse the packing process:
int get_move_source(int move) { return move & 0x7F; }
int get_move_target(int move) { return (move >> 7) & 0x7F; }
int get_move_promoted(int move) { return (move >> 14) & 0xF; }
int get_move_capture(int move) { return (move >> 18) & 0x1; }
int get_move_push(int move) { return (move >> 19) & 0x1; }
int get_move_enpassant(int move) { return (move >> 20) & 0x1; }
int get_move_castling(int move) { return (move >> 21) & 0x1; }
The general pattern is to shift the desired field back to the lowest bits and then apply a mask. The mask removes everything except the bits belonging to that particular field.
For example, 0x7F keeps seven bits, which is enough for a 0x88 square index. The mask 0xF keeps four bits for the promoted piece, while 0x1 extracts a single flag.
This gives the rest of the engine a simple interface. Code that needs the target square can call get_move_target(), while code checking whether a move is an en passant capture can call get_move_enpassant(). None of that code needs to know how the move is physically packed inside the integer.
Adding a Move to the List
After a move has been encoded, ABC adds it to the move list using add_move():
void add_move(Movelist *moves, int move) {
moves->moves[moves->count] = move;
moves->count++;
}
The move is stored at the current end of the list, and the move count is then increased. The function itself does not decide whether a move is legal or special. Its only responsibility is to append an already constructed move to the list.
Together, these functions form the basic move representation used throughout ABC. A move can contain its source square, target square, promoted piece, capture status, double pawn push status, en passant status, and castling status, all inside one compact integer.
Detecting Attacked Squares
One of the most important questions in chess is whether a particular square is under attack. ABC needs this information for several parts of the engine, including checking whether a king is in check and determining whether certain moves are legal.
The function is_square_attacked() answers this question by examining every piece type that could attack the given square:
int is_square_attacked(int square, int color) {
for (int piece_type = KING; piece_type <= QUEEN; piece_type++) {
int piece = piece_type | (color << 3);
if (piece_type == PAWN) {
int direction = 16 * (1 - 2 * color);
for (int left_right = -1; left_right <= 1; left_right += 2) {
int target = square + direction + left_right;
if (!(target & 0x88) && board[target] == piece) return 1;
}
}
else {
int slider = piece_type & 0x04;
for (int d = 0; d < offset_length[piece_type]; d++) {
int target = square;
do {
target += move_offsets[piece_type][d];
if (target & 0x88) break;
int attacker = board[target];
if (attacker != EMPTY) {
if (attacker == piece) return 1;
break;
}
} while (slider);
}
}
}
return 0;
}
The implementation is not the fastest possible way to detect attacks, but that is an intentional trade-off in ABC. The engine is optimized first for understanding, so the logic is kept compact and easy to follow. As an exercise, try writing a faster version of is_square_attacked(). What would you change?
The Elegance of Piece Encoding
A particularly elegant part of this function is the way ABC constructs the piece it is looking for:
int piece = piece_type | (color << 3);
ABC's piece encoding stores the piece type in the lower bits and the color in the upper part of the value. This means the same expression can construct either the White or Black version of any piece without needing separate tables or long chains of conditions.
The loop therefore does not need to ask whether it is searching for a White king, Black king, White bishop, Black bishop, and so on. It simply combines the current piece type with the requested color and compares the result directly with the board.
This is a good example of how a carefully designed internal representation can make later code significantly simpler.
Pawns
Pawns are handled separately because their attack pattern is different from the other pieces. Their attack direction depends on their color, so ABC first calculates the appropriate forward direction:
int direction = 16 * (1 - 2 * color);
From the square being tested, the function checks the two diagonally adjacent squares in that direction. The 0x88 test ensures that the calculated square is actually on the board before accessing board[target].
If either square contains the expected pawn, the square is attacked and the function immediately returns 1.
Pieces and Sliding Attacks
All other piece types use the movement information already stored in move_offsets and offset_length. This allows kings, knights, bishops, rooks, and queens to be handled by the same general mechanism.
The slider value determines whether the piece can continue moving along the same direction:
int slider = piece_type & 0x04;
Sliding pieces such as bishops, rooks, and queens continue along a direction until they reach the edge of the board or encounter another piece. Kings and knights only need to examine their immediate destination squares.
When another piece is encountered, ABC first checks whether it is the piece being searched for. If it is, the square is attacked. If it is a different piece, the path is blocked and that direction can no longer contain an attacker:
if (attacker != EMPTY) {
if (attacker == piece) return 1;
break;
}
The 0x88 representation makes the boundary check especially simple. A target outside the valid board has its fourth bit set, so target & 0x88 immediately detects it.
One Function, All Attackers
The important idea is that ABC does not need a separate attack-detection routine for every piece. The piece encoding, movement offsets, offset lengths, and 0x88 board representation work together to make one compact function possible.
As soon as any matching attacker is found, the function returns 1. If every possible piece type has been checked without finding one, the final return value is 0, meaning the square is not attacked.
Generating Moves
With move encoding and attack detection in place, ABC can finally generate chess moves. This is where the pieces of the previous sections come together: the 0x88 board, piece encoding, movement offsets, pawn ranks, castling rights, en passant state, and compact move representation all work together inside one function.
The function generates pseudo-legal moves. This means the moves follow the movement rules of the pieces, but the generator does not yet remove moves that leave the moving side's king in check. Those moves are filtered later when ABC actually tries to make them.
void generate_moves(Movelist *moves) {
moves->count = 0;
for (int src = 0; src < 128; src++) {
if (!(src & 0x88)) {
int piece = board[src];
int piece_type = piece & 7;
if ((piece >> 3) == side) {
if (piece_type == PAWN) {
int direction = -16 * (1 - 2 * side);
int dst = src + direction;
if ((dst & 0x88) == 0 && board[dst] == EMPTY) {
if ((dst & 0xF0) == pawn_promoting_rank[side]) {
for (int promoted_piece = QUEEN; promoted_piece >= KNIGHT; promoted_piece--)
add_move(moves, encode_move(src, dst, (promoted_piece | (side << 3)), 1, 0, 0, 0));
} else {
add_move(moves, encode_move(src, dst, 0, 0, 0, 0, 0));
int double_dst = src + direction * 2;
if ((src & 0xF0) == pawn_starting_rank[side] && board[double_dst] == EMPTY)
add_move(moves, encode_move(src, double_dst, 0, 0, 1, 0, 0));
}
}
for (int lr = -1; lr <= 1; lr += 2) {
dst = src + direction + lr;
if (dst & 0x88) continue;
int dst_piece = board[dst];
if (dst_piece != EMPTY && (dst_piece >> 3) != side) {
if ((dst & 0xF0) == pawn_promoting_rank[side]) {
for (int promoted_piece = QUEEN; promoted_piece >= KNIGHT; promoted_piece--)
add_move(moves, encode_move(src, dst, (promoted_piece | (side << 3)), 1, 0, 0, 0));
} else {
add_move(moves, encode_move(src, dst, 0, 1, 0, 0, 0));
}
}
if (dst == enpassant)
add_move(moves, encode_move(src, dst, 0, 1, 0, 1, 0));
}
}
else if (piece_type == KING) {
int ks = king_square[side];
if (castle & castling_side[side][0]) {
if (board[ks + 1] == EMPTY && board[ks + 2] == EMPTY) {
if (is_square_attacked(ks, 1 - side) == 0 && is_square_attacked(ks + 1, 1 - side) == 0)
add_move(moves, encode_move(ks, ks + 2, 0, 0, 0, 0, 1));
}
}
if (castle & castling_side[side][1]) {
if (board[ks - 1] == EMPTY && board[ks - 2] == EMPTY && board[ks - 3] == EMPTY) {
if (is_square_attacked(ks, 1 - side) == 0 &&
is_square_attacked(ks - 1, 1 - side) == 0)
add_move(moves, encode_move(ks, ks - 2, 0, 0, 0, 0, 1));
}
}
}
if (piece_type != PAWN) {
int slider = piece_type & 0x04;
for (int d = 0; d < offset_length[piece_type]; d++) {
int dst = src;
do {
dst += move_offsets[piece_type][d];
if (dst & 0x88) break;
int dst_piece = board[dst];
if (dst_piece != EMPTY) {
if ((dst_piece >> 3) != side)
add_move(moves, encode_move(src, dst, 0, 1, 0, 0, 0));
break;
}
add_move(moves, encode_move(src, dst, 0, 0, 0, 0, 0));
} while (slider);
}
}
}
}
}
}
Starting with an Empty Move List
Every call begins by resetting the move counter:
moves->count = 0;
The move array itself does not need to be cleared. Once the count is reset, the generator can simply overwrite the entries as new moves are added.
Walking Through the 0x88 Board
The generator examines every index from 0 to 127:
for (int src = 0; src < 128; src++) {
if (!(src & 0x88)) {
...
}
}
This is another place where the simplicity of 0x88 becomes valuable. The loop can cover the entire 128-element array, while src & 0x88 immediately filters out the unused squares between the ranks.
For every valid square, ABC reads the piece and extracts its type:
int piece = board[src];
int piece_type = piece & 7;
The color is extracted just as elegantly:
if ((piece >> 3) == side)
Because of the piece encoding, the generator does not need separate loops for White and Black. It simply checks the color encoded inside the piece and processes only the pieces belonging to the side to move.
Generating Pawn Moves
Pawns are the most specialized pieces in chess, so they receive their own section of the generator. Their movement direction is calculated from the side to move:
int direction = -16 * (1 - 2 * side);
The first test handles the pawn's ordinary forward move. The destination must be on the board and empty:
int dst = src + direction;
if ((dst & 0x88) == 0 && board[dst] == EMPTY)
If the pawn reaches its promotion rank, ABC generates the possible promoted pieces instead of an ordinary pawn move. The loop creates promotions to queen, rook, bishop, and knight.
Otherwise, the ordinary pawn move is added. If the pawn is on its starting rank and the square two steps ahead is also empty, ABC adds the double pawn push and sets the push flag in the encoded move.
Pawn Captures and En Passant
Pawn captures are generated by checking the two diagonal squares. The loop:
for (int lr = -1; lr <= 1; lr += 2)
produces -1 and +1, allowing the same code to examine both capture directions.
If the destination contains an enemy piece, a capture is added. Promotion captures are handled in exactly the same way as ordinary pawn promotions, except that the capture flag is set.
En passant is even simpler because the current en passant square is already stored globally. If the pawn's destination matches enpassant, ABC adds the move with both the capture and en passant flags set:
if (dst == enpassant)
add_move(moves, encode_move(src, dst, 0, 1, 0, 1, 0));
Castling
Castling is handled separately from ordinary king movement because it depends on several conditions beyond the king's normal movement pattern.
First, ABC checks the appropriate castling-rights bit. It then verifies that the required squares are empty and that the king is not currently in check or passing through an attacked square.
For kingside castling, the king moves two squares toward the rook. For queenside castling, it moves two squares in the opposite direction. The generated move has the castling flag set:
add_move(moves, encode_move(ks, ks + 2, 0, 0, 0, 0, 1));
The rook itself does not need to be added as a separate move. The move is represented as one castling move, and the rook's movement is handled later when the move is made.
Generating King, Knight, Bishop, Rook, and Queen Moves
After pawns and castling have been handled, the remaining pieces can use the common movement system built around move_offsets and offset_length.
The generator again uses the piece encoding to determine whether the piece is a slider:
int slider = piece_type & 0x04;
For every allowed direction, ABC starts at the source square and repeatedly applies the corresponding offset. A non-sliding piece stops after one destination, while a sliding piece continues until it reaches the edge of the board or encounters another piece.
When an occupied square is encountered, ABC checks its color. An enemy piece can be captured, but a friendly piece blocks the path:
if (dst_piece != EMPTY) {
if ((dst_piece >> 3) != side)
add_move(moves, encode_move(src, dst, 0, 1, 0, 0, 0));
break;
}
If the destination is empty, an ordinary non-capturing move is added and a sliding piece continues along the same direction.
Why This Generator Is So Compact
The impressive part of generate_moves() is not simply that it generates moves, but how much of the chess rules are handled by the data structures ABC has already established. The 0x88 board provides boundary detection, piece encoding provides type and color, movement tables provide directions, pawn rank tables provide promotion and double-push information, and move encoding stores all special properties in a single integer.
As a result, the generator does not need a huge collection of piece-specific functions. The data representation does much of the work, allowing one relatively small function to describe the movement rules of the entire game.
And this is still only pseudo-legal move generation. ABC deliberately separates generating moves from verifying whether the king is left in check. That distinction becomes important now when we are reaching move making.
Making a Move
Generating moves is only half of the problem. Once ABC has a move encoded as a single integer, the engine needs a way to actually apply that move to the current position. This is the responsibility of make_move().
This function is one of the most important pieces of the engine because a chess move can change much more than two squares on the board. A pawn may promote, an en passant capture removes a pawn from a different square, a double pawn push creates a new en passant target, castling moves a rook as well as the king, castling rights can disappear, the king's position may change, and finally the move must be rejected if it leaves the moving side's king in check.
ABC keeps all of these responsibilities together in one function:
int make_move(int move, int capture_flag) {
// Make all moves
if (capture_flag == ALL_MOVES) {
// Preserve board position
Position position;
save_position(&position);
// Decode move
int from_square = get_move_source(move);
int to_square = get_move_target(move);
int promoted_piece = get_move_promoted(move);
int ep = get_move_enpassant(move);
int double_push = get_move_push(move);
int castling = get_move_castling(move);
// Move piece
board[to_square] = board[from_square];
board[from_square] = EMPTY;
// Pawn promotion
if (promoted_piece) board[to_square] = promoted_piece;
// Enpassant capture
if (ep) !side ? (board[to_square + 16] = EMPTY) : (board[to_square - 16] = EMPTY);
enpassant = NONE;
if (double_push) !side ? (enpassant = to_square + 16) : (enpassant = to_square - 16);
// Castling move
if (castling) {
switch(to_square) {
case G1: board[F1] = board[H1]; board[H1] = EMPTY; break;
case C1: board[D1] = board[A1]; board[A1] = EMPTY; break;
case G8: board[F8] = board[H8]; board[H8] = EMPTY; break;
case C8: board[D8] = board[A8]; board[A8] = EMPTY; break;
}
}
// Update castling rights
castle &= castling_rights[from_square];
castle &= castling_rights[to_square];
// Update king square
if (board[to_square] == WK || board[to_square] == BK)
king_square[side] = to_square;
// Switch side to move
side ^= 1;
// Filter illegal moves
if (is_square_attacked(!side ? king_square[side ^ 1] : king_square[side ^ 1], side)) {
restore_position(&position);
return 0;
} else return 1;
}
// Make only captures
else {
if (get_move_capture(move)) return make_move(move, ALL_MOVES);
else return 0;
}
}
One Function, Two Jobs
The first design decision is visible in the function parameters. make_move() does not only mean “make any move.” The second argument determines what kind of moves the caller wants to allow.
int make_move(int move, int capture_flag)
When capture_flag is ALL_MOVES, the complete move-making process is performed. When it is ONLY_CAPTURES, the function first checks whether the encoded move is a capture:
if (get_move_capture(move)) return make_move(move, ALL_MOVES);
else return 0;
This gives ABC a surprisingly elegant interface. The engine does not need a second function such as generate_captures() just for quiescence search. The same move-making machinery is reused, with the caller deciding whether all moves or only captures should be accepted.
In the main search, negamax uses ALL_MOVES because it needs to examine every possible move:
make_move(move, ALL_MOVES)
Quiescence search, on the other hand, uses ONLY_CAPTURES because it is interested only in tactical captures:
make_move(move, ONLY_CAPTURES)
This is a good example of the design philosophy behind ABC: reuse the same mechanism instead of creating several almost-identical functions.
There is a price, though. This abstraction is not necessarily the fastest possible solution. ONLY_CAPTURES still enters make_move(), checks the capture flag, and then calls the full implementation for captures. A highly optimized engine might use specialized capture-making code to remove some of this overhead.
Once again, it is a trade-off. ABC chooses simpler and more reusable code over squeezing every last instruction out of the hot path.
Preserving the Position
Before changing anything, ABC saves the complete current position:
Position position;
save_position(&position);
This is essential because the move may turn out to be illegal. The generator produces pseudo-legal moves, meaning that a move can obey all the movement rules of the piece while still exposing its own king to attack.
Instead of trying to predict this during move generation, ABC simply makes the move, checks the resulting position, and restores the previous state if necessary.
This is another important separation of responsibilities:
generate_moves() generates possibilities.
make_move() determines whether a possibility is actually legal.
Decoding the Move
The move integer contains everything needed to perform the operation. ABC extracts the relevant fields using the getters introduced earlier:
int from_square = get_move_source(move);
int to_square = get_move_target(move);
int promoted_piece = get_move_promoted(move);
int ep = get_move_enpassant(move);
int double_push = get_move_push(move);
int castling = get_move_castling(move);
Notice what is not decoded here: the capture flag. For the full move-making path, the board itself already tells ABC what occupies the destination square. The capture flag is primarily useful for deciding whether a move should be accepted by the ONLY_CAPTURES mode.
The Ordinary Move
The basic operation is wonderfully small:
board[to_square] = board[from_square];
board[from_square] = EMPTY;
The piece is copied to its destination and the source square becomes empty. If the destination contained an opponent's piece, it is naturally overwritten.
This is another benefit of the board representation. A normal capture does not need a special “remove captured piece” operation. Writing the moving piece onto the destination square automatically replaces the captured piece.
Promotion
Promotion is handled immediately after the ordinary move:
if (promoted_piece) board[to_square] = promoted_piece;
The pawn has already moved to the promotion square, but the encoded move also contains the piece it should become. ABC simply replaces the pawn with that promoted piece.
Because the promoted piece already contains its color in its encoding, there is no additional color calculation here. The move generator prepared the correct piece value when it encoded the promotion.
En Passant
En passant is different from an ordinary capture because the captured pawn is not located on the destination square. The moving pawn lands on the en passant target square, while the captured pawn remains one rank behind it.
ABC removes that pawn with a compact conditional expression:
if (ep) !side ? (board[to_square + 16] = EMPTY) : (board[to_square - 16] = EMPTY);
The direction depends on the side that is making the move. After removing the captured pawn, the en passant square is cleared:
enpassant = NONE;
This is important because an en passant target exists for only one move. If the current move was a double pawn push, ABC creates the new target square instead:
if (double_push) !side ? (enpassant = to_square + 16) : (enpassant = to_square - 16);
So the same section both consumes the previous en passant opportunity and creates the next one when appropriate.
Castling
Castling is represented as a single move in the move list, but two pieces actually move on the board. The king has already been moved by the ordinary move code, so ABC only needs to relocate the rook:
if (castling) {
switch(to_square) {
case G1: board[F1] = board[H1]; board[H1] = EMPTY; break;
case C1: board[D1] = board[A1]; board[A1] = EMPTY; break;
case G8: board[F8] = board[H8]; board[H8] = EMPTY; break;
case C8: board[D8] = board[A8]; board[A8] = EMPTY; break;
}
}
The destination square of the king is enough to identify which castling operation is being performed. There is no need for a separate flag saying “kingside” or “queenside.” The four possible king destinations uniquely determine the rook's movement.
Updating Castling Rights
Castling rights are not permanent. Moving a king removes both castling rights for that side, moving a rook can remove one right, and capturing a rook can also remove the corresponding right.
ABC handles all of these cases with two bitwise AND operations:
castle &= castling_rights[from_square];
castle &= castling_rights[to_square];
The first mask represents what rights remain after something leaves the source square. The second represents what rights remain after something arrives on the destination square.
This is an excellent example of why the castling-rights table was prepared earlier. Instead of writing a collection of special cases such as “if the king moved, clear these bits” or “if the rook on A1 was captured, clear that bit,” the move-making code simply applies the masks associated with the two affected squares.
Tracking the King
ABC also maintains the location of each king separately:
if (board[to_square] == WK || board[to_square] == BK)
king_square[side] = to_square;
This saves the engine from having to search the entire board whenever it needs to know where a king is. The information is updated only when the moving piece is a king.
This becomes particularly important in the next stage of move making, because the engine must determine whether the king is attacked after the move.
Changing the Side to Move
Once all board-state changes have been made, ABC switches the side to move:
side ^= 1;
The XOR operation toggles the side between WHITE and BLACK. If White was moving, it becomes Black's turn; if Black was moving, it becomes White's turn.
The timing here is important. The legality test below asks whether the king of the side that just moved is attacked by the new side to move. Switching side first makes that relationship straightforward.
Filtering Illegal Moves
This is the final and most important step:
if (is_square_attacked(!side ? king_square[side ^ 1] : king_square[side ^ 1], side)) {
restore_position(&position);
return 0;
} else return 1;
At this point the move has already been played. ABC asks whether the king belonging to the side that just moved is now attacked by the opponent.
If the answer is yes, the move was illegal. The saved position is restored and make_move() returns 0:
restore_position(&position);
return 0;
If the king is safe, the modified position is kept and the function returns 1:
return 1;
This is the point where the distinction between pseudo-legal and legal moves finally becomes concrete. generate_moves() can generate a move that exposes the king. make_move() detects that situation and rejects it.
The Complete Flow
A useful way to understand make_move() is to follow the position through the function in order:
save position
↓
decode move
↓
move piece
↓
handle promotion
↓
handle en passant
↓
update en passant target
↓
move rook for castling
↓
update castling rights
↓
update king location
↓
switch side
↓
check whether king is attacked
↓
restore if illegal
↓
accept if legal
Every special chess rule fits somewhere in this sequence. The result is a function that takes one compact move integer and transforms the complete engine state into the next position—or completely undoes the operation if the move was illegal.
A Small Function with a Large Responsibility
make_move() is a good demonstration of the philosophy behind ABC. The function is not trying to be the most specialized or aggressively optimized move-making routine possible. Instead, it reuses the structures that have already been built: encoded moves, the 0x88 board, castling masks, king-square tracking, attack detection, and position saving.
Most importantly, the same function serves two different parts of the search. Negamax calls it with ALL_MOVES, while quiescence search calls it with ONLY_CAPTURES. That avoids maintaining a separate capture-only move-making implementation.
It is elegant because one mechanism does several jobs. It is slower than a heavily specialized implementation because that elegance introduces a little extra work. And that is exactly the kind of trade-off ABC is designed to demonstrate: simpler code can be more valuable for learning, even when a faster implementation exists.
Saving and Restoring a Position
ABC uses two small functions to preserve and restore the current chess position. Together, they implement the copy-make approach used by make_move().
// Preserve current position state
void save_position(Position *position) {
memcpy(position->board, board, sizeof(board));
memcpy(position->king_square, king_square, sizeof(king_square));
position->side = side;
position->enpassant = enpassant;
position->castle = castle;
}
// Restore preserved position state
void restore_position(Position *position) {
memcpy(board, position->board, sizeof(board));
memcpy(king_square, position->king_square, sizeof(king_square));
side = position->side;
enpassant = position->enpassant;
castle = position->castle;
}
Saving the Position
save_position() copies every part of the chess state that can be changed by making a move.
memcpy(position->board, board, sizeof(board));
memcpy(position->king_square, king_square, sizeof(king_square));
The entire 0x88 board is copied, along with the locations of both kings. The remaining state is copied directly:
position->side = side;
position->enpassant = enpassant;
position->castle = castle;
This gives Position a complete snapshot of the current chess position.
Restoring the Position
restore_position() performs the exact opposite operation. The saved board and king locations are copied back, while the remaining state variables are restored directly.
memcpy(board, position->board, sizeof(board));
memcpy(king_square, position->king_square, sizeof(king_square));
side = position->side;
enpassant = position->enpassant;
castle = position->castle;
After this function finishes, the position is back exactly where it was when save_position() was called.
Why Copy-Make?
This approach saves ABC from having to implement a separate take_back() routine that knows how to reverse every possible change made by a move.
With a traditional incremental make/unmake design, the engine would have to remember additional information about every move. For example, if a piece was captured, the engine would need to know which piece was captured so that it could be restored later.
Copy-make avoids that bookkeeping. ABC simply saves the entire position before changing it and restores the snapshot when necessary.
The trade-off is performance: copying the whole position is more expensive than carefully recording and reversing only the changes. But for ABC, the simplicity is worth it. The code is smaller, easier to understand, and there is no complicated take_back() implementation to maintain.