@@ -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