diff --git a/include/bitbishop/movegen/bishop_moves.hpp b/include/bitbishop/movegen/bishop_moves.hpp index 830c2ac7..b00ae732 100644 --- a/include/bitbishop/movegen/bishop_moves.hpp +++ b/include/bitbishop/movegen/bishop_moves.hpp @@ -45,7 +45,8 @@ * computed for the current position. */ inline void generate_bishop_legal_moves(std::vector& moves, const Board& board, Color us, - const Bitboard& check_mask, const PinResult& pins) { + const Bitboard& check_mask, const PinResult& pins, + const Bitboard& allowed_targets = Bitboard::Ones()) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); const Bitboard occupied = board.occupied(); @@ -58,6 +59,7 @@ inline void generate_bishop_legal_moves(std::vector& moves, const Board& b Bitboard candidates = bishop_attacks(from, occupied); candidates &= ~own; candidates &= check_mask; + candidates &= allowed_targets; if (pins.pinned.test(from)) { candidates &= pins.pin_ray[from.value()]; } diff --git a/include/bitbishop/movegen/king_moves.hpp b/include/bitbishop/movegen/king_moves.hpp index 2edafdbe..3c5bb0b8 100644 --- a/include/bitbishop/movegen/king_moves.hpp +++ b/include/bitbishop/movegen/king_moves.hpp @@ -8,7 +8,8 @@ #include inline void generate_legal_king_moves(std::vector& moves, const Board& board, Color us, Square king_sq, - const Bitboard& enemy_attacks) { + const Bitboard& enemy_attacks, + const Bitboard& allowed_targets = Bitboard::Ones()) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); @@ -16,6 +17,7 @@ inline void generate_legal_king_moves(std::vector& moves, const Board& boa candidates &= ~own; candidates &= ~enemy_attacks; + candidates &= allowed_targets; for (Square to : candidates) { const bool is_capture = enemy.test(to); diff --git a/include/bitbishop/movegen/knight_moves.hpp b/include/bitbishop/movegen/knight_moves.hpp index b1d2c022..4fa1d7e4 100644 --- a/include/bitbishop/movegen/knight_moves.hpp +++ b/include/bitbishop/movegen/knight_moves.hpp @@ -10,7 +10,8 @@ // pinned knights cannot move at all due to knight's l-shaped move geometry inline void generate_knight_legal_moves(std::vector& moves, const Board& board, Color us, - const Bitboard& check_mask, const PinResult& pins) { + const Bitboard& check_mask, const PinResult& pins, + const Bitboard& allowed_targets = Bitboard::Ones()) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); Bitboard knights = board.knights(us); @@ -23,6 +24,7 @@ inline void generate_knight_legal_moves(std::vector& moves, const Board& b Bitboard candidates = Lookups::KNIGHT_ATTACKS[from.value()]; candidates &= ~own; candidates &= check_mask; + candidates &= allowed_targets; for (Square to : candidates) { const bool is_capture = enemy.test(to); diff --git a/include/bitbishop/movegen/legal_moves.hpp b/include/bitbishop/movegen/legal_moves.hpp index f5aabe06..e89e46b0 100644 --- a/include/bitbishop/movegen/legal_moves.hpp +++ b/include/bitbishop/movegen/legal_moves.hpp @@ -18,29 +18,16 @@ #include #include -/** - * @brief Generates all legal moves for the given side in the current position. - * - * This function computes the full set of legal moves for @p us, taking into - * account checks, pins, enemy attacks, and special rules such as castling - * and en passant. Move legality is enforced during generation rather than - * by post-filtering. - * - * The generation pipeline is: - * - Detect checkers and compute the check resolution mask - * - Compute pinned pieces and their allowed movement rays - * - Generate legal king moves (always allowed) - * - Generate legal castling moves (only when not in check) - * - If in double check, stop after king moves - * - Otherwise, generate legal moves for all remaining pieces - * - * @param moves Vector to append generated legal moves to - * @param board Current board position - * - * @note The move list is appended to; it is not cleared by this function. - * @note Assumes the board position is internally consistent and legal. - */ -inline void generate_legal_moves(std::vector& moves, const Board& board) { +namespace MoveGen { +enum class Scope { + AllMoves, + CapturesOnly, +}; +} // namespace MoveGen + +inline void generate_legal_moves_with_scope(std::vector& moves, const Board& board, MoveGen::Scope scope) { + const bool captures_only = scope == MoveGen::Scope::CapturesOnly; + Color us = board.get_state().m_is_white_turn ? Color::WHITE : Color::BLACK; Color them = ColorUtil::opposite(us); @@ -50,18 +37,39 @@ inline void generate_legal_moves(std::vector& moves, const Board& board) { Bitboard check_mask = compute_check_mask(king_sq, checkers, board); PinResult pins = compute_pins(king_sq, board, us); Bitboard enemy_attacks = generate_attacks(board, them); + Bitboard enemy = board.enemy(us); + Bitboard allowed_targets = captures_only ? enemy : Bitboard::Ones(); - generate_legal_king_moves(moves, board, us, king_sq, enemy_attacks); + generate_legal_king_moves(moves, board, us, king_sq, enemy_attacks, allowed_targets); - generate_castling_moves(moves, board, us, checkers, enemy_attacks); + if (!captures_only) { + generate_castling_moves(moves, board, us, checkers, enemy_attacks); + } if (checkers.count() > 1) { return; } - generate_knight_legal_moves(moves, board, us, check_mask, pins); - generate_bishop_legal_moves(moves, board, us, check_mask, pins); - generate_rook_legal_moves(moves, board, us, check_mask, pins); - generate_queen_legal_moves(moves, board, us, check_mask, pins); - generate_pawn_legal_moves(moves, board, us, king_sq, check_mask, pins); + generate_knight_legal_moves(moves, board, us, check_mask, pins, allowed_targets); + generate_bishop_legal_moves(moves, board, us, check_mask, pins, allowed_targets); + generate_rook_legal_moves(moves, board, us, check_mask, pins, allowed_targets); + generate_queen_legal_moves(moves, board, us, check_mask, pins, allowed_targets); + generate_pawn_legal_moves(moves, board, us, king_sq, check_mask, pins, captures_only); +} + +/** + * @brief Generates all legal moves for the side to move. + */ +inline void generate_legal_moves(std::vector& moves, const Board& board) { + generate_legal_moves_with_scope(moves, board, MoveGen::Scope::AllMoves); +} + +/** + * @brief Generates only legal capture moves for the side to move. + * + * Includes king captures, piece captures, pawn captures and legal en passant. + * Excludes non-capture moves (quiet king moves, pawn pushes, castling, ...). + */ +inline void generate_legal_capture_moves(std::vector& moves, const Board& board) { + generate_legal_moves_with_scope(moves, board, MoveGen::Scope::CapturesOnly); } diff --git a/include/bitbishop/movegen/pawn_moves.hpp b/include/bitbishop/movegen/pawn_moves.hpp index 174d765f..6a51fe9b 100644 --- a/include/bitbishop/movegen/pawn_moves.hpp +++ b/include/bitbishop/movegen/pawn_moves.hpp @@ -254,7 +254,8 @@ inline void generate_en_passant(std::vector& moves, Square from, Color us, * @param pins Pin result structure indicating which pieces are pinned */ inline void generate_pawn_legal_moves(std::vector& moves, const Board& board, Color us, Square king_sq, - const Bitboard& check_mask, const PinResult& pins) { + const Bitboard& check_mask, const PinResult& pins, + bool captures_only = false) { const Bitboard enemy = board.enemy(us); const Bitboard occupied = board.occupied(); Bitboard pawns = board.pawns(us); @@ -264,8 +265,10 @@ inline void generate_pawn_legal_moves(std::vector& moves, const Board& boa const bool is_pinned = pins.pinned.test(from); const Bitboard pin_mask = is_pinned ? pins.pin_ray[from.flat_index()] : Bitboard::Ones(); - generate_single_push(moves, from, us, occupied, check_mask, pin_mask); - generate_double_push(moves, from, us, occupied, check_mask, pin_mask); + if (!captures_only) { + generate_single_push(moves, from, us, occupied, check_mask, pin_mask); + generate_double_push(moves, from, us, occupied, check_mask, pin_mask); + } generate_captures(moves, from, us, enemy, check_mask, pin_mask); generate_en_passant(moves, from, us, board, king_sq, check_mask, pin_mask); } diff --git a/include/bitbishop/movegen/queen_moves.hpp b/include/bitbishop/movegen/queen_moves.hpp index f1389c2a..275ff778 100644 --- a/include/bitbishop/movegen/queen_moves.hpp +++ b/include/bitbishop/movegen/queen_moves.hpp @@ -46,7 +46,8 @@ * - Promotions, en passant, and castling are not applicable to queen moves. */ inline void generate_queen_legal_moves(std::vector& moves, const Board& board, Color us, - const Bitboard& check_mask, const PinResult& pins) { + const Bitboard& check_mask, const PinResult& pins, + const Bitboard& allowed_targets = Bitboard::Ones()) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); const Bitboard occupied = board.occupied(); @@ -59,6 +60,7 @@ inline void generate_queen_legal_moves(std::vector& moves, const Board& bo Bitboard candidates = queen_attacks(from, occupied); candidates &= ~own; candidates &= check_mask; + candidates &= allowed_targets; if (pins.pinned.test(from)) { candidates &= pins.pin_ray[from.value()]; } diff --git a/include/bitbishop/movegen/rook_moves.hpp b/include/bitbishop/movegen/rook_moves.hpp index 9b0c72ce..c1c70cef 100644 --- a/include/bitbishop/movegen/rook_moves.hpp +++ b/include/bitbishop/movegen/rook_moves.hpp @@ -46,7 +46,8 @@ * - Promotions, en passant, and castling are not applicable to rook moves. */ inline void generate_rook_legal_moves(std::vector& moves, const Board& board, Color us, - const Bitboard& check_mask, const PinResult& pins) { + const Bitboard& check_mask, const PinResult& pins, + const Bitboard& allowed_targets = Bitboard::Ones()) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); const Bitboard occupied = board.occupied(); @@ -59,6 +60,7 @@ inline void generate_rook_legal_moves(std::vector& moves, const Board& boa Bitboard candidates = rook_attacks(from, occupied); candidates &= ~own; candidates &= check_mask; + candidates &= allowed_targets; if (pins.pinned.test(from)) { candidates &= pins.pin_ray[from.value()]; } diff --git a/src/bitbishop/engine/search.cpp b/src/bitbishop/engine/search.cpp index 630d9d0a..edd6408c 100644 --- a/src/bitbishop/engine/search.cpp +++ b/src/bitbishop/engine/search.cpp @@ -34,17 +34,12 @@ int Search::quiesce(Position& position, int alpha, int beta, SearchStats& stats, } std::vector moves; - generate_legal_moves(moves, board); - // Optimization: implement generate_capture_moves(moves, board); instead - // To generate only capture moves and not all moves top discard some in the end + generate_legal_capture_moves(moves, board); for (const Move& move : moves) { if (stop_flag != nullptr && stop_flag->load()) { return alpha; } - if (!move.is_capture) { - continue; - } position.apply_move(move); // Quiescence window flip: child is searched with (-beta, -alpha) and the returned score is negated. diff --git a/tests/bitbishop/movegen/test_legal_capture_moves.cpp b/tests/bitbishop/movegen/test_legal_capture_moves.cpp new file mode 100644 index 00000000..964cd06c --- /dev/null +++ b/tests/bitbishop/movegen/test_legal_capture_moves.cpp @@ -0,0 +1,112 @@ +#include + +#include +#include +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +TEST(GenerateLegalCaptureMovesTest, StartingPositionHasNoCaptures) { + Board board = Board::StartingPosition(); + + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + + std::vector moves; + generate_legal_capture_moves(moves, board); + + EXPECT_TRUE(moves.empty()); +} + +TEST(GenerateLegalCaptureMovesTest, ExcludesQuietMovesAndCastling) { + Board board("r3k2r/8/8/8/r7/8/8/R3K2R w KQkq - 0 1"); + + std::vector moves; + generate_legal_capture_moves(moves, board); + + EXPECT_GT(moves.size(), 0); + EXPECT_TRUE(contains_move(moves, {A1, A4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {H1, H8, std::nullopt, true, false, false})); + + EXPECT_FALSE(contains_move(moves, {E1, G1, std::nullopt, false, false, true})); + EXPECT_FALSE(contains_move(moves, {E1, C1, std::nullopt, false, false, true})); + EXPECT_FALSE(contains_move(moves, {A1, A2, std::nullopt, false, false, false})); + + for (const Move& move : moves) { + EXPECT_TRUE(move.is_capture); + } +} + +TEST(GenerateLegalCaptureMovesTest, IncludesEnPassantCapture) { + Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); + + std::vector moves; + generate_legal_capture_moves(moves, board); + + EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); + EXPECT_EQ(count_en_passant(moves), 1); + for (const Move& move : moves) { + EXPECT_TRUE(move.is_capture); + } +} + +TEST(GenerateLegalCaptureMovesTest, IncludesCapturePromotionsOnly) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(A8, BLACK_KING); + board.set_piece(G7, WHITE_PAWN); + board.set_piece(H8, BLACK_ROOK); + + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + + std::vector moves; + generate_legal_capture_moves(moves, board); + + EXPECT_EQ(moves.size(), 4); + EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_QUEEN, true, false, false})); + EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_ROOK, true, false, false})); + EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_BISHOP, true, false, false})); + EXPECT_TRUE(contains_move(moves, {G7, H8, WHITE_KNIGHT, true, false, false})); + + EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_QUEEN, false, false, false})); + EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_ROOK, false, false, false})); + EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_BISHOP, false, false, false})); + EXPECT_FALSE(contains_move(moves, {G7, G8, WHITE_KNIGHT, false, false, false})); +} + +TEST(GenerateLegalCaptureMovesTest, InCheckOnlyLegalCaptureResponses) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(B5, WHITE_BISHOP); + board.set_piece(E8, BLACK_ROOK); + board.set_piece(H8, BLACK_KING); + board.set_piece(A6, BLACK_PAWN); + + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + + std::vector moves; + generate_legal_capture_moves(moves, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_TRUE(contains_move(moves, {B5, E8, std::nullopt, true, false, false})); + EXPECT_FALSE(contains_move(moves, {B5, A6, std::nullopt, true, false, false})); +} + +TEST(GenerateLegalCaptureMovesTest, MovesVectorIsAppendedNotCleared) { + Board board = Board::StartingPosition(); + std::vector moves; + moves.emplace_back(Move::make(A1, A2)); + + generate_legal_capture_moves(moves, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_TRUE(contains_move(moves, Move::make(A1, A2))); +}