Skip to content

Commit 03030d3

Browse files
committed
bugfix on en passant rule
1 parent 3adc219 commit 03030d3

File tree

2 files changed

+302
-23
lines changed

2 files changed

+302
-23
lines changed

include/bitbishop/movegen/pawn_moves.hpp

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#pragma once
22

3+
#include <bitbishop/attacks/generate_attacks.hpp>
34
#include <bitbishop/board.hpp>
45
#include <bitbishop/color.hpp>
5-
#include <bitbishop/lookups/attackers.hpp>
66
#include <bitbishop/lookups/pawn_attacks.hpp>
77
#include <bitbishop/move.hpp>
88
#include <bitbishop/movegen/pins.hpp>
@@ -102,6 +102,19 @@ void add_pawn_promotions(std::vector<Move>& moves, Square from, Square to, Color
102102
}
103103
}
104104

105+
/**
106+
* @brief Generates all single-square pawn pushes from the given square.
107+
*
108+
* Applies occupancy, check mask, and pin mask filters. If the target square
109+
* is a promotion rank, all promotion moves are added.
110+
*
111+
* @param moves Vector to append generated moves to
112+
* @param from Source square of the pawn
113+
* @param us Color of the pawn
114+
* @param occupied Bitboard of all occupied squares
115+
* @param check_mask Bitboard mask to restrict moves under check
116+
* @param pin_mask Bitboard mask to restrict moves due to pins
117+
*/
105118
inline void generate_single_push(std::vector<Move>& moves, Square from, Color us, const Bitboard& occupied,
106119
const Bitboard& check_mask, const Bitboard& pin_mask) {
107120
const auto& single_push = Lookups::PAWN_SINGLE_PUSH[ColorUtil::to_index(us)];
@@ -121,6 +134,53 @@ inline void generate_single_push(std::vector<Move>& moves, Square from, Color us
121134
}
122135
}
123136

137+
/**
138+
* @brief Generates all double-square pawn pushes from the given square.
139+
*
140+
* Only allowed from the starting rank. Ensures the intermediate square is empty
141+
* and respects occupancy, check mask, and pin mask constraints.
142+
*
143+
* @param moves Vector to append generated moves to
144+
* @param from Source square of the pawn
145+
* @param us Color of the pawn
146+
* @param occupied Bitboard of all occupied squares
147+
* @param check_mask Bitboard mask to restrict moves under check
148+
* @param pin_mask Bitboard mask to restrict moves due to pins
149+
*/
150+
inline void generate_double_push(std::vector<Move>& moves, Square from, Color us, const Bitboard& occupied,
151+
const Bitboard& check_mask, const Bitboard& pin_mask) {
152+
const auto& single_push = Lookups::PAWN_SINGLE_PUSH[ColorUtil::to_index(us)];
153+
const auto& double_push = Lookups::PAWN_DOUBLE_PUSH[ColorUtil::to_index(us)];
154+
155+
if (!is_starting_rank(from, us)) {
156+
return;
157+
}
158+
159+
Bitboard bb = double_push[from.flat_index()];
160+
bb &= ~occupied;
161+
bb &= check_mask;
162+
bb &= pin_mask;
163+
164+
Bitboard single_bb = single_push[from.flat_index()] & occupied;
165+
if (single_bb.empty() && bb) {
166+
Square to = bb.pop_lsb().value();
167+
moves.emplace_back(from, to, std::nullopt, false, false, false);
168+
}
169+
}
170+
171+
/**
172+
* @brief Generates all pawn capture moves from the given square.
173+
*
174+
* Only captures squares occupied by enemy pieces. Applies check mask and pin mask.
175+
* Automatically adds promotion moves if the destination is on the promotion rank.
176+
*
177+
* @param moves Vector to append generated moves to
178+
* @param from Source square of the pawn
179+
* @param us Color of the pawn
180+
* @param enemy Bitboard of enemy pieces
181+
* @param check_mask Bitboard mask to restrict moves under check
182+
* @param pin_mask Bitboard mask to restrict moves due to pins
183+
*/
124184
inline void generate_captures(std::vector<Move>& moves, Square from, Color us, const Bitboard& enemy,
125185
const Bitboard& check_mask, const Bitboard& pin_mask) {
126186
const auto& captures = Lookups::PAWN_ATTACKS[ColorUtil::to_index(us)];
@@ -139,6 +199,20 @@ inline void generate_captures(std::vector<Move>& moves, Square from, Color us, c
139199
}
140200
}
141201

202+
/**
203+
* @brief Generates a pawn en passant move from the given square, if legal.
204+
*
205+
* Verifies en passant square exists, the pawn can capture, and that
206+
* performing the move does not leave the king in check.
207+
*
208+
* @param moves Vector to append generated moves to
209+
* @param from Source square of the pawn
210+
* @param us Color of the pawn
211+
* @param board Current board position
212+
* @param king_sq Square of the king for this side
213+
* @param check_mask Bitboard mask to restrict moves under check
214+
* @param pin_mask Bitboard mask to restrict moves due to pins
215+
*/
142216
inline void generate_en_passant(std::vector<Move>& moves, Square from, Color us, const Board& board, Square king_sq,
143217
const Bitboard& check_mask, const Bitboard& pin_mask) {
144218
const std::optional<Square> epsq_opt = board.en_passant_square();
@@ -147,6 +221,7 @@ inline void generate_en_passant(std::vector<Move>& moves, Square from, Color us,
147221
return;
148222
}
149223

224+
const Color them = ColorUtil::opposite(us);
150225
Square epsq = epsq_opt.value();
151226
auto bb = Bitboard(epsq);
152227
bb &= check_mask;
@@ -163,32 +238,27 @@ inline void generate_en_passant(std::vector<Move>& moves, Square from, Color us,
163238
tmp.remove_piece(cap_sq);
164239
tmp.move_piece(from, epsq);
165240

166-
if (!attackers_to(king_sq, us)) {
167-
moves.emplace_back(from, epsq, std::nullopt, true, true, false);
168-
}
169-
}
170-
171-
inline void generate_double_push(std::vector<Move>& moves, Square from, Color us, const Bitboard& occupied,
172-
const Bitboard& check_mask, const Bitboard& pin_mask) {
173-
const auto& single_push = Lookups::PAWN_SINGLE_PUSH[ColorUtil::to_index(us)];
174-
const auto& double_push = Lookups::PAWN_DOUBLE_PUSH[ColorUtil::to_index(us)];
241+
Bitboard attackers = generate_attacks(tmp, them);
175242

176-
if (!is_starting_rank(from, us)) {
177-
return;
178-
}
179-
180-
Bitboard bb = double_push[from.flat_index()];
181-
bb &= ~occupied;
182-
bb &= check_mask;
183-
bb &= pin_mask;
184-
185-
Bitboard single_bb = single_push[from.flat_index()] & occupied;
186-
if (single_bb.empty() && bb) {
187-
Square to = bb.pop_lsb().value();
188-
moves.emplace_back(from, to, std::nullopt, false, false, false);
243+
if (!attackers.test(king_sq)) {
244+
moves.emplace_back(from, epsq, std::nullopt, true, true, false);
189245
}
190246
}
191247

248+
/**
249+
* @brief Generates all legal pawn moves for the given side.
250+
*
251+
* Includes single pushes, double pushes, captures, promotions, and en passant,
252+
* taking into account occupancy, checks, and pins. Iterates over all pawns of
253+
* the specified color.
254+
*
255+
* @param moves Vector to append generated moves to
256+
* @param board Current board position
257+
* @param us Color of the side to generate moves for
258+
* @param king_sq Square of the king for this side
259+
* @param check_mask Bitboard mask to restrict moves under check
260+
* @param pins Pin result structure indicating which pieces are pinned
261+
*/
192262
void generate_pawn_legal_moves(std::vector<Move>& moves, const Board& board, Color us, Square king_sq,
193263
const Bitboard& check_mask, const PinResult& pins) {
194264
const Bitboard enemy = board.enemy(us);

tests/bitbishop/movegen/test_pawn_moves/test_generate_pawn_legal_moves.cpp

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,3 +549,212 @@ TEST(GeneratePawnLegalMovesTest, NoDoublePushOffStartingRank) {
549549
EXPECT_TRUE(contains_move(moves, {E3, E4, std::nullopt, false, false, false}));
550550
EXPECT_FALSE(contains_move(moves, {E3, E5, std::nullopt, false, false, false}));
551551
}
552+
553+
/**
554+
* @test White en passant capture.
555+
* @brief Confirms generate_pawn_legal_moves() generates en passant capture
556+
* for white pawn.
557+
*/
558+
TEST(GeneratePawnLegalMovesTest, WhiteEnPassantCapture) {
559+
Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1");
560+
561+
std::vector<Move> moves;
562+
Bitboard check_mask = Bitboard::Ones();
563+
PinResult pins = compute_pins(E1, board, Color::WHITE);
564+
565+
generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins);
566+
567+
// Should include en passant capture
568+
EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false}));
569+
}
570+
571+
/**
572+
* @test Black en passant capture.
573+
* @brief Confirms generate_pawn_legal_moves() generates en passant capture
574+
* for black pawn.
575+
*/
576+
TEST(GeneratePawnLegalMovesTest, BlackEnPassantCapture) {
577+
Board board("rnbqkbnr/ppp1pppp/8/8/3pP3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1");
578+
579+
std::vector<Move> moves;
580+
Bitboard check_mask = Bitboard::Ones();
581+
PinResult pins = compute_pins(E8, board, Color::BLACK);
582+
583+
generate_pawn_legal_moves(moves, board, Color::BLACK, E8, check_mask, pins);
584+
585+
// Should include en passant capture
586+
EXPECT_TRUE(contains_move(moves, {D4, E3, std::nullopt, true, true, false}));
587+
}
588+
589+
/**
590+
* @test En passant on both sides.
591+
* @brief Confirms generate_pawn_legal_moves() generates en passant when
592+
* two pawns can capture same target.
593+
*/
594+
TEST(GeneratePawnLegalMovesTest, EnPassantBothSides) {
595+
Board board("rnbqkbnr/ppp1pppp/8/2PpP3/8/8/PP1P1PPP/RNBQKBNR w KQkq d6 0 1");
596+
597+
std::vector<Move> moves;
598+
Bitboard check_mask = Bitboard::Ones();
599+
PinResult pins = compute_pins(E1, board, Color::WHITE);
600+
601+
generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins);
602+
603+
// Both pawns can capture en passant
604+
EXPECT_TRUE(contains_move(moves, {C5, D6, std::nullopt, true, true, false}));
605+
EXPECT_TRUE(contains_move(moves, {E5, D6, std::nullopt, true, true, false}));
606+
}
607+
608+
/**
609+
* @test No en passant without target square.
610+
* @brief Confirms generate_pawn_legal_moves() does not generate en passant
611+
* when no en passant square is set.
612+
*/
613+
TEST(GeneratePawnLegalMovesTest, NoEnPassantWithoutTarget) {
614+
Board board = Board::Empty();
615+
board.set_piece(E1, WHITE_KING);
616+
board.set_piece(D5, WHITE_PAWN);
617+
board.set_piece(E5, BLACK_PAWN);
618+
619+
std::vector<Move> moves;
620+
Bitboard check_mask = Bitboard::Ones();
621+
PinResult pins;
622+
623+
generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins);
624+
625+
// No en passant without target square
626+
EXPECT_FALSE(contains_move(moves, {D5, E6, std::nullopt, true, true, false}));
627+
}
628+
629+
/**
630+
* @test En passant blocked by check mask.
631+
* @brief Confirms generate_pawn_legal_moves() does not generate en passant
632+
* when target square not in check mask.
633+
*/
634+
TEST(GeneratePawnLegalMovesTest, EnPassantBlockedByCheckMask) {
635+
Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1");
636+
637+
Bitboard check_mask = Bitboard::Zeros();
638+
check_mask.set(D6); // En passant square E6 not in mask
639+
640+
std::vector<Move> moves;
641+
PinResult pins = compute_pins(E1, board, Color::WHITE);
642+
643+
generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins);
644+
645+
// En passant not allowed
646+
EXPECT_FALSE(contains_move(moves, {D5, E6, std::nullopt, true, true, false}));
647+
}
648+
649+
/**
650+
* @test En passant blocked by pin.
651+
* @brief Confirms generate_pawn_legal_moves() does not generate en passant
652+
* when pawn is pinned perpendicular to en passant direction.
653+
*/
654+
TEST(GeneratePawnLegalMovesTest, EnPassantBlockedByPin) {
655+
Board board = Board::Empty();
656+
board.set_piece(E1, WHITE_KING);
657+
board.set_piece(E5, WHITE_PAWN);
658+
board.set_piece(D5, BLACK_PAWN);
659+
board.set_piece(E8, BLACK_ROOK);
660+
661+
// Manually set en passant square
662+
Board board_with_ep("8/8/8/3pP3/8/8/8/4K2r b - e6 0 1");
663+
664+
std::vector<Move> moves;
665+
Bitboard check_mask = Bitboard::Ones();
666+
PinResult pins = compute_pins(E1, board_with_ep, Color::WHITE);
667+
668+
generate_pawn_legal_moves(moves, board_with_ep, Color::WHITE, E1, check_mask, pins);
669+
670+
// Pinned pawn cannot capture en passant
671+
EXPECT_FALSE(contains_move(moves, {E5, D6, std::nullopt, true, true, false}));
672+
}
673+
674+
/**
675+
* @test En passant reveals check (horizontal pin).
676+
* @brief Confirms generate_pawn_legal_moves() does not generate en passant
677+
* when capturing would expose king to horizontal attack.
678+
*/
679+
TEST(GeneratePawnLegalMovesTest, EnPassantRevealsHorizontalCheck) {
680+
// White king on E5, white pawn on D5, black pawn on E5, black rook on A5
681+
// If white captures en passant on E6, removes both pawns and exposes king
682+
Board board("8/8/8/r2PpK2/8/8/8/8 w - e6 0 1");
683+
684+
std::vector<Move> moves;
685+
Bitboard check_mask = Bitboard::Ones();
686+
PinResult pins = compute_pins(F5, board, Color::WHITE);
687+
688+
generate_pawn_legal_moves(moves, board, Color::WHITE, F5, check_mask, pins);
689+
690+
// En passant not allowed as it would expose king
691+
EXPECT_FALSE(contains_move(moves, {D5, E6, std::nullopt, true, true, false}));
692+
}
693+
694+
/**
695+
* @test En passant legal when safe.
696+
* @brief Confirms generate_pawn_legal_moves() generates en passant when
697+
* it doesn't expose king to check.
698+
*/
699+
TEST(GeneratePawnLegalMovesTest, EnPassantLegalWhenSafe) {
700+
Board board("rnbqkbnr/ppp2ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1");
701+
702+
std::vector<Move> moves;
703+
Bitboard check_mask = Bitboard::Ones();
704+
PinResult pins = compute_pins(E1, board, Color::WHITE);
705+
706+
generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins);
707+
708+
// En passant is safe
709+
EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false}));
710+
}
711+
712+
/**
713+
* @test En passant with promotion (edge case).
714+
* @brief Confirms en passant is only available on correct ranks
715+
* (5th for white, 4th for black).
716+
*/
717+
TEST(GeneratePawnLegalMovesTest, EnPassantOnlyOnCorrectRank) {
718+
// White pawn on 6th rank - cannot do en passant from here
719+
Board board = Board::Empty();
720+
board.set_piece(E1, WHITE_KING);
721+
board.set_piece(D6, WHITE_PAWN);
722+
board.set_piece(E6, BLACK_PAWN);
723+
724+
std::vector<Move> moves;
725+
Bitboard check_mask = Bitboard::Ones();
726+
PinResult pins;
727+
728+
generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins);
729+
730+
// No en passant from 6th rank
731+
EXPECT_FALSE(contains_move(moves, {D6, E7, std::nullopt, true, true, false}));
732+
}
733+
734+
/**
735+
* @test En passant move properties correct.
736+
* @brief Confirms en passant move has correct flags set.
737+
*/
738+
TEST(GeneratePawnLegalMovesTest, EnPassantMoveProperties) {
739+
Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1");
740+
741+
std::vector<Move> moves;
742+
Bitboard check_mask = Bitboard::Ones();
743+
PinResult pins = compute_pins(E1, board, Color::WHITE);
744+
745+
generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins);
746+
747+
// Find en passant move
748+
bool found = false;
749+
for (const Move& move : moves) {
750+
if (move.from == D5 && move.to == E6) {
751+
EXPECT_TRUE(move.is_capture);
752+
EXPECT_TRUE(move.is_en_passant);
753+
EXPECT_FALSE(move.is_castling);
754+
EXPECT_FALSE(move.promotion.has_value());
755+
found = true;
756+
break;
757+
}
758+
}
759+
EXPECT_TRUE(found);
760+
}

0 commit comments

Comments
 (0)