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
4 changes: 3 additions & 1 deletion include/bitbishop/movegen/bishop_moves.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
* computed for the current position.
*/
inline void generate_bishop_legal_moves(std::vector<Move>& 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();
Expand All @@ -58,6 +59,7 @@ inline void generate_bishop_legal_moves(std::vector<Move>& 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()];
}
Expand Down
4 changes: 3 additions & 1 deletion include/bitbishop/movegen/king_moves.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
#include <vector>

inline void generate_legal_king_moves(std::vector<Move>& 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);

Bitboard candidates = Lookups::KING_ATTACKS[king_sq.value()];

candidates &= ~own;
candidates &= ~enemy_attacks;
candidates &= allowed_targets;

for (Square to : candidates) {
const bool is_capture = enemy.test(to);
Expand Down
4 changes: 3 additions & 1 deletion include/bitbishop/movegen/knight_moves.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Move>& 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);
Expand All @@ -23,6 +24,7 @@ inline void generate_knight_legal_moves(std::vector<Move>& 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);
Expand Down
68 changes: 38 additions & 30 deletions include/bitbishop/movegen/legal_moves.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,16 @@
#include <bitbishop/movegen/rook_moves.hpp>
#include <vector>

/**
* @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<Move>& moves, const Board& board) {
namespace MoveGen {
enum class Scope {
AllMoves,
CapturesOnly,
};
} // namespace MoveGen

inline void generate_legal_moves_with_scope(std::vector<Move>& 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);

Expand All @@ -50,18 +37,39 @@ inline void generate_legal_moves(std::vector<Move>& 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<Move>& 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<Move>& moves, const Board& board) {
generate_legal_moves_with_scope(moves, board, MoveGen::Scope::CapturesOnly);
}
9 changes: 6 additions & 3 deletions include/bitbishop/movegen/pawn_moves.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ inline void generate_en_passant(std::vector<Move>& moves, Square from, Color us,
* @param pins Pin result structure indicating which pieces are pinned
*/
inline void generate_pawn_legal_moves(std::vector<Move>& 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);
Expand All @@ -264,8 +265,10 @@ inline void generate_pawn_legal_moves(std::vector<Move>& 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);
}
Expand Down
4 changes: 3 additions & 1 deletion include/bitbishop/movegen/queen_moves.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
* - Promotions, en passant, and castling are not applicable to queen moves.
*/
inline void generate_queen_legal_moves(std::vector<Move>& 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();
Expand All @@ -59,6 +60,7 @@ inline void generate_queen_legal_moves(std::vector<Move>& 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()];
}
Expand Down
4 changes: 3 additions & 1 deletion include/bitbishop/movegen/rook_moves.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
* - Promotions, en passant, and castling are not applicable to rook moves.
*/
inline void generate_rook_legal_moves(std::vector<Move>& 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();
Expand All @@ -59,6 +60,7 @@ inline void generate_rook_legal_moves(std::vector<Move>& 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()];
}
Expand Down
7 changes: 1 addition & 6 deletions src/bitbishop/engine/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,12 @@ int Search::quiesce(Position& position, int alpha, int beta, SearchStats& stats,
}

std::vector<Move> 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.
Expand Down
112 changes: 112 additions & 0 deletions tests/bitbishop/movegen/test_legal_capture_moves.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include <gtest/gtest.h>

#include <bitbishop/board.hpp>
#include <bitbishop/helpers/moves.hpp>
#include <bitbishop/move.hpp>
#include <bitbishop/movegen/legal_moves.hpp>
#include <bitbishop/square.hpp>

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<Move> 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<Move> 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<Move> 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<Move> 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<Move> 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<Move> 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)));
}
Loading