From 8d2043c6033899f050db89265d9518cb95579efa Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Mon, 5 Jan 2026 23:07:25 +0100 Subject: [PATCH 1/7] added bitwise exclusive or ^ operator to bitboard class --- include/bitbishop/bitboard.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/bitbishop/bitboard.hpp b/include/bitbishop/bitboard.hpp index 829c659..99da1cb 100644 --- a/include/bitbishop/bitboard.hpp +++ b/include/bitbishop/bitboard.hpp @@ -147,6 +147,7 @@ class Bitboard { constexpr bool operator==(const Bitboard& other) const { return m_bb == other.m_bb; } constexpr bool operator!=(const Bitboard& other) const { return m_bb != other.m_bb; } + constexpr Bitboard operator^(const Bitboard& other) const { return m_bb ^ other.m_bb; } constexpr Bitboard operator|(const Bitboard& other) const { return m_bb | other.m_bb; } constexpr Bitboard operator&(const Bitboard& other) const { return m_bb & other.m_bb; } constexpr Bitboard operator~() const { return ~m_bb; } @@ -160,6 +161,10 @@ class Bitboard { m_bb &= other.m_bb; return *this; } + constexpr Bitboard& operator^=(const Bitboard& other) { + m_bb ^= other.m_bb; + return *this; + } constexpr operator bool() const noexcept { return m_bb != 0ULL; } /** From 5587ef41200970e1ecc507ea54eecbf0fa0464e7 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Mon, 5 Jan 2026 23:12:34 +0100 Subject: [PATCH 2/7] updated generate attacks to x-ray kings --- .../bitbishop/attacks/generate_attacks.hpp | 24 +++++++---------- .../attacks/test_generate_attacks.cpp | 27 +++++++++++++++---- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/include/bitbishop/attacks/generate_attacks.hpp b/include/bitbishop/attacks/generate_attacks.hpp index 0494be1..647c1e8 100644 --- a/include/bitbishop/attacks/generate_attacks.hpp +++ b/include/bitbishop/attacks/generate_attacks.hpp @@ -11,19 +11,15 @@ #include /** - * @brief Computes the set of squares attacked by all pieces of a given side. + * @brief Computes the set of squares attacked by all pieces of a given side, + * assuming the opposing king is not present on the board. * - * Generates a bitboard containing every square currently attacked by the - * specified color, based on the current board occupancy. This includes - * attacks from the king, knights, pawns, and all sliding pieces (rooks, - * bishops, queens). + * This attack map is suitable for validating king moves and castling legality. + * Sliding attacks (rook, bishop, queen) are generated with the opposing king + * removed from occupancy to correctly model x-ray attacks. * * The result represents geometric attacks only and does not account for - * move legality, pins, discovered checks, or whether the attacking pieces - * themselves are defended. - * - * This function is primarily used for king safety evaluation, including - * legal king move generation and castling validation. + * move legality, pins, or whether attacking pieces are defended. * * @param board The current board position. * @param enemy The side whose attacks are to be generated. @@ -32,7 +28,7 @@ Bitboard generate_attacks(const Board& board, Color enemy) { Bitboard attacks = Bitboard::Zeros(); - Bitboard occupied = board.occupied(); + Bitboard occupied_no_king = board.occupied() ^ board.king(ColorUtil::opposite(enemy)); Bitboard king = board.king(enemy); if (king.any()) { @@ -62,19 +58,19 @@ Bitboard generate_attacks(const Board& board, Color enemy) { Bitboard rooks = board.rooks(enemy); while (rooks) { Square sq = rooks.pop_lsb().value(); - attacks |= rook_attacks(sq, occupied); + attacks |= rook_attacks(sq, occupied_no_king); } Bitboard bishops = board.bishops(enemy); while (bishops) { Square sq = bishops.pop_lsb().value(); - attacks |= bishop_attacks(sq, occupied); + attacks |= bishop_attacks(sq, occupied_no_king); } Bitboard queens = board.queens(enemy); while (queens) { Square sq = queens.pop_lsb().value(); - attacks |= queen_attacks(sq, occupied); + attacks |= queen_attacks(sq, occupied_no_king); } return attacks; diff --git a/tests/bitbishop/attacks/test_generate_attacks.cpp b/tests/bitbishop/attacks/test_generate_attacks.cpp index e970ee1..ca507bf 100644 --- a/tests/bitbishop/attacks/test_generate_attacks.cpp +++ b/tests/bitbishop/attacks/test_generate_attacks.cpp @@ -166,10 +166,8 @@ TEST(GenerateAttacksTest, MultiplePawnsAttacks) { TEST(GenerateAttacksTest, PawnOnEdgeAttacks) { Board board = Board::Empty(); board.set_piece(A4, BLACK_PAWN); - board.print(); Bitboard attacks = generate_attacks(board, Color::BLACK); - attacks.print(); EXPECT_EQ(attacks.count(), 1); EXPECT_TRUE(attacks.test(B3)); @@ -451,11 +449,11 @@ TEST(GenerateAttacksTest, AttacksIncludeFriendlySquares) { * @brief Confirms generate_attacks() does not include squares beyond * blockers (no x-ray vision). */ -TEST(GenerateAttacksTest, NoXRayAttacks) { +TEST(GenerateAttacksTest, NoXRayAttacksThroughAnyPieceThatIsNotKing) { Board board = Board::Empty(); board.set_piece(E4, BLACK_ROOK); - board.set_piece(E5, WHITE_KING); // Blocker - board.set_piece(E6, WHITE_PAWN); // Beyond blocker + board.set_piece(E5, WHITE_KNIGHT); // Blocker + board.set_piece(E6, WHITE_PAWN); // Beyond blocker Bitboard attacks = generate_attacks(board, Color::BLACK); @@ -464,6 +462,25 @@ TEST(GenerateAttacksTest, NoXRayAttacks) { EXPECT_FALSE(attacks.test(E7)); // Beyond blocker } +/** + * @test X-ray attacks included only for king pieces. + * @brief Confirms generate_attacks() includes squares beyond + * blockers if there is a king. + */ +TEST(GenerateAttacksTest, XRayAttacksThroughKing) { + Board board = Board::Empty(); + board.set_piece(E4, BLACK_ROOK); + board.set_piece(E5, WHITE_KING); // Blocker + board.set_piece(E7, WHITE_PAWN); // Beyond blocker + + Bitboard attacks = generate_attacks(board, Color::BLACK); + + EXPECT_TRUE(attacks.test(E5)); // Up to blocker + EXPECT_TRUE(attacks.test(E6)); // Beyond blocker (x-ray) + EXPECT_TRUE(attacks.test(E7)); // Beyond blocker + EXPECT_FALSE(attacks.test(E8)); // Blocked by non-king piece +} + /** * @test No king on board handled gracefully. * @brief Confirms generate_attacks() handles edge case where enemy From edd35f5da3e0bffcc20f27fb93155000f4023ab1 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Mon, 5 Jan 2026 23:14:12 +0100 Subject: [PATCH 3/7] removed check mask to king legal moves generation since it's not suited for this piece in particular --- include/bitbishop/movegen/king_moves.hpp | 3 +- tests/bitbishop/movegen/test_king_moves.cpp | 123 +++----------------- 2 files changed, 19 insertions(+), 107 deletions(-) diff --git a/include/bitbishop/movegen/king_moves.hpp b/include/bitbishop/movegen/king_moves.hpp index 4709437..b6c3034 100644 --- a/include/bitbishop/movegen/king_moves.hpp +++ b/include/bitbishop/movegen/king_moves.hpp @@ -8,7 +8,7 @@ #include void generate_legal_king_moves(std::vector& moves, const Board& board, Color us, Square king_sq, - const Bitboard& enemy_attacks, const Bitboard& check_mask) { + const Bitboard& enemy_attacks) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); @@ -16,7 +16,6 @@ void generate_legal_king_moves(std::vector& moves, const Board& board, Col candidates &= ~own; candidates &= ~enemy_attacks; - candidates &= check_mask; for (Square to : candidates) { const bool is_capture = enemy.test(to); diff --git a/tests/bitbishop/movegen/test_king_moves.cpp b/tests/bitbishop/movegen/test_king_moves.cpp index a81e0b4..160a16b 100644 --- a/tests/bitbishop/movegen/test_king_moves.cpp +++ b/tests/bitbishop/movegen/test_king_moves.cpp @@ -21,9 +21,8 @@ TEST(GenerateLegalKingMovesTest, CenterKingEmptyBoard) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); EXPECT_EQ(moves.size(), 8); @@ -49,9 +48,8 @@ TEST(GenerateLegalKingMovesTest, CornerKingLimitedMoves) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, A1, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, A1, enemy_attacks); EXPECT_EQ(moves.size(), 3); @@ -72,9 +70,8 @@ TEST(GenerateLegalKingMovesTest, EdgeKingLimitedMoves) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E1, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E1, enemy_attacks); EXPECT_EQ(moves.size(), 5); @@ -100,9 +97,8 @@ TEST(GenerateLegalKingMovesTest, FriendlyPiecesBlocked) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); EXPECT_EQ(moves.size(), 5); @@ -134,9 +130,8 @@ TEST(GenerateLegalKingMovesTest, EnemyAttacksBlocked) { enemy_attacks.set(D5); std::vector moves; - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); EXPECT_EQ(moves.size(), 5); @@ -153,49 +148,6 @@ TEST(GenerateLegalKingMovesTest, EnemyAttacksBlocked) { EXPECT_TRUE(contains_move(moves, {E4, F5, std::nullopt, false, false, false})); } -/** - * @test Check mask restricts king moves. - * @brief Confirms generate_legal_king_moves() only generates moves within - * the check mask. - */ -TEST(GenerateLegalKingMovesTest, CheckMaskRestriction) { - Board board = Board::Empty(); - board.set_piece(E4, WHITE_KING); - - Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Zeros(); - check_mask.set(D3); - check_mask.set(E3); - - std::vector moves; - - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); - - EXPECT_EQ(moves.size(), 2); - - // Only moves to D3 and E3 should be generated - EXPECT_TRUE(contains_move(moves, {E4, D3, std::nullopt, false, false, false})); - EXPECT_TRUE(contains_move(moves, {E4, E3, std::nullopt, false, false, false})); -} - -/** - * @test Empty check mask generates no moves. - * @brief Confirms generate_legal_king_moves() generates no moves when - * check mask is empty (double check). - */ -TEST(GenerateLegalKingMovesTest, EmptyCheckMaskNoMoves) { - Board board = Board::Empty(); - board.set_piece(E4, WHITE_KING); - - std::vector moves; - Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Zeros(); - - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); - - EXPECT_EQ(moves.size(), 0); -} - /** * @test King captures enemy pieces. * @brief Confirms generate_legal_king_moves() generates capture moves @@ -209,9 +161,8 @@ TEST(GenerateLegalKingMovesTest, KingCapturesEnemyPieces) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); // Should generate 8 moves (6 empty + 2 captures) EXPECT_EQ(moves.size(), 8); @@ -243,9 +194,8 @@ TEST(GenerateLegalKingMovesTest, CannotCaptureOnAttackedSquare) { enemy_attacks.set(D3); // Square is defended std::vector moves; - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); // Should generate 7 moves (D3 is excluded) EXPECT_EQ(moves.size(), 7); @@ -256,7 +206,7 @@ TEST(GenerateLegalKingMovesTest, CannotCaptureOnAttackedSquare) { /** * @test All restrictions combined. * @brief Confirms generate_legal_king_moves() correctly applies friendly - * pieces, enemy attacks, and check mask simultaneously. + * pieces and enemy attacks simultaneously. */ TEST(GenerateLegalKingMovesTest, AllRestrictionsCombined) { Board board = Board::Empty(); @@ -270,20 +220,17 @@ TEST(GenerateLegalKingMovesTest, AllRestrictionsCombined) { enemy_attacks.set(E5); // Knight square defended enemy_attacks.set(F3); // Attacked by knight - Bitboard check_mask = Bitboard::Ones(); - check_mask.clear(F5); // Not in check mask - std::vector moves; - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); - // Available: D5, E3, F4 (3 moves) - // Blocked: D3 (friendly + attacked), D4 (attacked), E5 (attacked/defended), F3 (attacked by knight), F5 (not in mask) - EXPECT_EQ(moves.size(), 3); + // Available: D5, E3, F4, F5 (4 moves) + EXPECT_EQ(moves.size(), 4); EXPECT_TRUE(contains_move(moves, {E4, D5, std::nullopt, false, false, false})); EXPECT_TRUE(contains_move(moves, {E4, E3, std::nullopt, false, false, false})); EXPECT_TRUE(contains_move(moves, {E4, F4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E4, F5, std::nullopt, false, false, false})); } /** @@ -298,9 +245,8 @@ TEST(GenerateLegalKingMovesTest, MovePropertiesCorrect) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); for (const Move& move : moves) { EXPECT_EQ(move.from, E4); @@ -326,9 +272,8 @@ TEST(GenerateLegalKingMovesTest, BlackKingMoves) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::BLACK, E8, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::BLACK, E8, enemy_attacks); // Should generate 4 moves (D7 blocked, E7 capturable), D8, F8 and F7 free EXPECT_EQ(moves.size(), 4); @@ -364,9 +309,8 @@ TEST(GenerateLegalKingMovesTest, KingSurroundedNoMoves) { std::vector moves; Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); EXPECT_EQ(moves.size(), 0); } @@ -391,9 +335,8 @@ TEST(GenerateLegalKingMovesTest, AllSquaresAttackedNoMoves) { enemy_attacks.set(F5); std::vector moves; - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); EXPECT_EQ(moves.size(), 0); } @@ -412,43 +355,14 @@ TEST(GenerateLegalKingMovesTest, MovesVectorAccumulates) { moves.emplace_back(A1, A2, std::nullopt, false, false, false); Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); // Should have 1 dummy + 8 king moves = 9 total EXPECT_EQ(moves.size(), 9); EXPECT_TRUE(contains_move(moves, {A1, A2, std::nullopt, false, false, false})); } -/** - * @test Partial check mask allows some moves. - * @brief Confirms generate_legal_king_moves() correctly filters moves - * based on non-trivial check mask patterns. - */ -TEST(GenerateLegalKingMovesTest, PartialCheckMask) { - Board board = Board::Empty(); - board.set_piece(E4, WHITE_KING); - - Bitboard enemy_attacks = Bitboard::Zeros(); - Bitboard check_mask = Bitboard::Zeros(); - check_mask.set(D3); - check_mask.set(D4); - check_mask.set(D5); - check_mask.set(E3); - - std::vector moves; - - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); - - EXPECT_EQ(moves.size(), 4); - - EXPECT_TRUE(contains_move(moves, {E4, D3, std::nullopt, false, false, false})); - EXPECT_TRUE(contains_move(moves, {E4, D4, std::nullopt, false, false, false})); - EXPECT_TRUE(contains_move(moves, {E4, D5, std::nullopt, false, false, false})); - EXPECT_TRUE(contains_move(moves, {E4, E3, std::nullopt, false, false, false})); -} - /** * @test King captures undefended enemy pieces only. * @brief Confirms generate_legal_king_moves() allows captures of @@ -464,9 +378,8 @@ TEST(GenerateLegalKingMovesTest, CaptureUndefendedOnly) { enemy_attacks.set(E5); // E5 is defended std::vector moves; - Bitboard check_mask = Bitboard::Ones(); - generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, Color::WHITE, E4, enemy_attacks); // Verify D3 capture is allowed EXPECT_TRUE(contains_move(moves, {E4, D3, std::nullopt, true, false, false})); From 6e551771d90d76b8d266bbf3c1110be2453dd4da Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Mon, 5 Jan 2026 23:14:58 +0100 Subject: [PATCH 4/7] removed print statement in tests --- tests/bitbishop/movegen/test_pins/test_compute_pins.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/bitbishop/movegen/test_pins/test_compute_pins.cpp b/tests/bitbishop/movegen/test_pins/test_compute_pins.cpp index 44c502b..8902f71 100644 --- a/tests/bitbishop/movegen/test_pins/test_compute_pins.cpp +++ b/tests/bitbishop/movegen/test_pins/test_compute_pins.cpp @@ -298,8 +298,6 @@ TEST(ComputePinsTest, MaximumPinsAllDirections) { board.set_piece(D3, WHITE_PAWN); board.set_piece(B1, BLACK_BISHOP); - board.print(); - PinResult result = compute_pins(E4, board, Color::WHITE); EXPECT_EQ(result.pinned.count(), 8); From 65e7890c613e364b46f6778f3f0397908fb448e9 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Mon, 5 Jan 2026 23:42:27 +0100 Subject: [PATCH 5/7] added xor bitboard operator tests --- .../bitboard/test_bb_bitwise_ops.cpp | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/tests/bitbishop/bitboard/test_bb_bitwise_ops.cpp b/tests/bitbishop/bitboard/test_bb_bitwise_ops.cpp index 7f1d2fc..11ef2d3 100644 --- a/tests/bitbishop/bitboard/test_bb_bitwise_ops.cpp +++ b/tests/bitbishop/bitboard/test_bb_bitwise_ops.cpp @@ -36,10 +36,28 @@ TEST(BitboardTest, AndOperatorFindsIntersection) { } /** - * @test Compound OR and AND operators. - * @brief Ensures |= adds squares and &= keeps only intersections. + * @test Exclusive bitwise (X)OR operator. + * @brief Returns the bitboard with common bits set to zero. */ -TEST(BitboardTest, CompoundOrAnd) { +TEST(BitboardTest, ExlusiveOrOperatorDeletesCommonBits) { + Bitboard a, b; + a.set(Square::E2); + a.set(Square::E4); + b.set(Square::E2); + b.set(Square::D2); + + Bitboard c = a ^ b; + + EXPECT_FALSE(c.test(Square::E2)); + EXPECT_TRUE(c.test(Square::E4)); + EXPECT_TRUE(c.test(Square::D2)); +} + +/** + * @test Compound OR operator. + * @brief Ensures |= adds squares. + */ +TEST(BitboardTest, CompoundOr) { Bitboard a, b; a.set(Square::A1); b.set(Square::B2); @@ -47,14 +65,40 @@ TEST(BitboardTest, CompoundOrAnd) { a |= b; EXPECT_TRUE(a.test(Square::A1)); EXPECT_TRUE(a.test(Square::B2)); +} + +/** + * @test Compound AND operator. + * @brief Ensures &= keeps only intersections. + */ +TEST(BitboardTest, CompoundAnd) { + Bitboard a, b; + a.set(Square::A1); + a.set(Square::B2); + b.set(Square::A1); + a &= b; - Bitboard c; - c.set(Square::A1); - a &= c; EXPECT_TRUE(a.test(Square::A1)); EXPECT_FALSE(a.test(Square::B2)); } +/** + * @test Compound XOR. + * @brief Ensures ^= removes common set bits. + */ +TEST(BitboardTest, CompoundXOr) { + Bitboard a; + a.set(Square::A1); + a.set(Square::B3); + + Bitboard b; + b.set(Square::A1); + b ^= a; + + EXPECT_FALSE(b.test(Square::A1)); + EXPECT_TRUE(b.test(Square::B3)); +} + /** * @test Bitwise NOT operator. * @brief Inverts all bits: occupied squares become empty and vice versa. From ab43214e513e2ba18496f33af97e01adb0328b73 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Mon, 5 Jan 2026 23:46:17 +0100 Subject: [PATCH 6/7] implemented missing dynamic checkers computation (taking into account blocking pieces) --- include/bitbishop/attacks/checkers.hpp | 22 + src/bitbishop/attacks/checkers.cpp | 46 ++ .../checkers/test_compute_checkers.cpp | 510 ++++++++++++++++++ 3 files changed, 578 insertions(+) create mode 100644 include/bitbishop/attacks/checkers.hpp create mode 100644 src/bitbishop/attacks/checkers.cpp create mode 100644 tests/bitbishop/attacks/checkers/test_compute_checkers.cpp diff --git a/include/bitbishop/attacks/checkers.hpp b/include/bitbishop/attacks/checkers.hpp new file mode 100644 index 0000000..170b2ee --- /dev/null +++ b/include/bitbishop/attacks/checkers.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include + +/** + * @brief Computes the set of enemy pieces currently giving check to a king. + * + * Returns a bitboard containing the squares of all pieces of color @p attacker + * that are directly attacking the king on @p king_sq in the current position. + * + * The result is occupancy-aware and exact: + * - Sliding attacks respect blockers + * - Only real pieces are included + * - The bit count corresponds to single / double check + * + * @param board Current board position + * @param king_sq Square of the king being attacked + * @param attacker Color of the attacking side + * @return Bitboard of checking piece squares + */ +Bitboard compute_checkers(const Board& board, Square king_sq, Color attacker); diff --git a/src/bitbishop/attacks/checkers.cpp b/src/bitbishop/attacks/checkers.cpp new file mode 100644 index 0000000..2d97237 --- /dev/null +++ b/src/bitbishop/attacks/checkers.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Bitboard compute_checkers(const Board& board, Square king_sq, Color attacker) { + using namespace Lookups; + + Bitboard checkers; + Bitboard occupied = board.occupied(); + + checkers |= KNIGHT_ATTACKERS[king_sq.value()] & board.knights(attacker); + + if (attacker == Color::WHITE) { + checkers |= WHITE_PAWN_ATTACKERS[king_sq.value()] & board.pawns(attacker); + } else { + checkers |= BLACK_PAWN_ATTACKERS[king_sq.value()] & board.pawns(attacker); + } + + checkers |= KING_ATTACKERS[king_sq.value()] & board.king(attacker); + + Bitboard diag_sliders = BISHOP_ATTACKER_RAYS[king_sq.value()] & (board.bishops(attacker) | board.queens(attacker)); + while (diag_sliders) { + Square sq = diag_sliders.pop_lsb().value(); + if (bishop_attacks(sq, occupied).test(king_sq)) { + checkers.set(sq); + } + } + + Bitboard ortho_sliders = ROOK_ATTACKER_RAYS[king_sq.value()] & (board.rooks(attacker) | board.queens(attacker)); + while (ortho_sliders) { + Square sq = ortho_sliders.pop_lsb().value(); + if (rook_attacks(sq, occupied).test(king_sq)) { + checkers.set(sq); + } + } + + return checkers; +} diff --git a/tests/bitbishop/attacks/checkers/test_compute_checkers.cpp b/tests/bitbishop/attacks/checkers/test_compute_checkers.cpp new file mode 100644 index 0000000..24f2434 --- /dev/null +++ b/tests/bitbishop/attacks/checkers/test_compute_checkers.cpp @@ -0,0 +1,510 @@ +#include + +#include +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +/** + * @test No checkers on empty board. + * @brief Confirms compute_checkers() returns empty bitboard when no + * attacking pieces present. + */ +TEST(ComputeCheckersTest, NoCheckersEmptyBoard) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); + EXPECT_EQ(checkers.count(), 0); +} + +/** + * @test No checkers when pieces too far. + * @brief Confirms compute_checkers() returns empty when attacker pieces + * cannot reach king. + */ +TEST(ComputeCheckersTest, NoCheckersWhenTooFar) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(A8, BLACK_ROOK); + board.set_piece(H2, BLACK_BISHOP); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test Knight checking king. + * @brief Confirms compute_checkers() detects knight check. + */ +TEST(ComputeCheckersTest, KnightCheck) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(D2, BLACK_KNIGHT); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(D2)); +} + +/** + * @test Multiple knights checking king. + * @brief Confirms compute_checkers() detects multiple knight checks. + */ +TEST(ComputeCheckersTest, MultipleKnightChecks) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(D2, BLACK_KNIGHT); + board.set_piece(F2, BLACK_KNIGHT); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 2); + EXPECT_TRUE(checkers.test(D2)); + EXPECT_TRUE(checkers.test(F2)); +} + +/** + * @test White pawn checking black king. + * @brief Confirms compute_checkers() detects white pawn check. + */ +TEST(ComputeCheckersTest, WhitePawnCheck) { + Board board = Board::Empty(); + board.set_piece(E5, BLACK_KING); + board.set_piece(D4, WHITE_PAWN); + + Bitboard checkers = compute_checkers(board, E5, Color::WHITE); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(D4)); +} + +/** + * @test Black pawn checking white king. + * @brief Confirms compute_checkers() detects black pawn check. + */ +TEST(ComputeCheckersTest, BlackPawnCheck) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(D5, BLACK_PAWN); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(D5)); +} + +/** + * @test Multiple pawn checks. + * @brief Confirms compute_checkers() detects both pawns checking king. + */ +TEST(ComputeCheckersTest, MultiplePawnChecks) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(D5, BLACK_PAWN); + board.set_piece(F5, BLACK_PAWN); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 2); + EXPECT_TRUE(checkers.test(D5)); + EXPECT_TRUE(checkers.test(F5)); +} + +/** + * @test Pawn not checking from wrong direction. + * @brief Confirms compute_checkers() does not detect pawn check when + * pawn cannot attack king's square. + */ +TEST(ComputeCheckersTest, PawnNotCheckingWrongDirection) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(E5, BLACK_PAWN); // Pawns don't attack forward + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test King checking king (adjacent kings). + * @brief Confirms compute_checkers() detects when enemy king is adjacent. + */ +TEST(ComputeCheckersTest, KingCheckingKing) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(E5, BLACK_KING); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(E5)); +} + +/** + * @test Rook checking king orthogonally. + * @brief Confirms compute_checkers() detects rook check along file. + */ +TEST(ComputeCheckersTest, RookCheckAlongFile) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(E8)); +} + +/** + * @test Rook checking king along rank. + * @brief Confirms compute_checkers() detects rook check along rank. + */ +TEST(ComputeCheckersTest, RookCheckAlongRank) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(A4, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(A4)); +} + +/** + * @test Rook blocked does not check. + * @brief Confirms compute_checkers() does not detect check when rook + * is blocked. + */ +TEST(ComputeCheckersTest, RookBlockedNoCheck) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, BLACK_PAWN); + board.set_piece(E8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test Bishop checking king diagonally. + * @brief Confirms compute_checkers() detects bishop check along diagonal. + */ +TEST(ComputeCheckersTest, BishopCheckAlongDiagonal) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(A5, BLACK_BISHOP); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(A5)); +} + +/** + * @test Bishop blocked does not check. + * @brief Confirms compute_checkers() does not detect check when bishop + * is blocked. + */ +TEST(ComputeCheckersTest, BishopBlockedNoCheck) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(C3, BLACK_PAWN); + board.set_piece(A5, BLACK_BISHOP); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test Queen checking orthogonally. + * @brief Confirms compute_checkers() detects queen check along rank/file. + */ +TEST(ComputeCheckersTest, QueenCheckOrthogonally) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, BLACK_QUEEN); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(E8)); +} + +/** + * @test Queen checking diagonally. + * @brief Confirms compute_checkers() detects queen check along diagonal. + */ +TEST(ComputeCheckersTest, QueenCheckDiagonally) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(H4, BLACK_QUEEN); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(H4)); +} + +/** + * @test Queen blocked does not check. + * @brief Confirms compute_checkers() does not detect check when queen + * is blocked. + */ +TEST(ComputeCheckersTest, QueenBlockedNoCheck) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, WHITE_PAWN); + board.set_piece(E8, BLACK_QUEEN); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test Double check detected. + * @brief Confirms compute_checkers() detects multiple pieces checking king. + */ +TEST(ComputeCheckersTest, DoubleCheck) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, BLACK_ROOK); + board.set_piece(D3, BLACK_KNIGHT); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 2); + EXPECT_TRUE(checkers.test(E8)); + EXPECT_TRUE(checkers.test(D3)); +} + +/** + * @test Triple check detected. + * @brief Confirms compute_checkers() detects all checking pieces. + */ +TEST(ComputeCheckersTest, TripleCheck) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(E8, BLACK_ROOK); + board.set_piece(A4, BLACK_ROOK); + board.set_piece(D2, BLACK_KNIGHT); + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 3); + EXPECT_TRUE(checkers.test(E8)); + EXPECT_TRUE(checkers.test(A4)); + EXPECT_TRUE(checkers.test(D2)); +} + +/** + * @test Discovered check piece not marked as checker. + * @brief Confirms compute_checkers() only marks direct checkers, not + * pieces that moved to discover check. + */ +TEST(ComputeCheckersTest, DiscoveredCheckNotMarked) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(D2, BLACK_KNIGHT); // Moved, discovered check + board.set_piece(E8, BLACK_ROOK); // Actual checker + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + // Should only detect the rook as checker + EXPECT_TRUE(checkers.test(E8)); +} + +/** + * @test White attacking black king. + * @brief Confirms compute_checkers() works when white is attacker. + */ +TEST(ComputeCheckersTest, WhiteAttackingBlack) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + board.set_piece(E1, WHITE_ROOK); + + Bitboard checkers = compute_checkers(board, E8, Color::WHITE); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(E1)); +} + +/** + * @test Black attacking white king. + * @brief Confirms compute_checkers() works when black is attacker. + */ +TEST(ComputeCheckersTest, BlackAttackingWhite) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(E8)); +} + +/** + * @test Friendly pieces do not check own king. + * @brief Confirms compute_checkers() only considers pieces of attacker color. + */ +TEST(ComputeCheckersTest, FriendlyPiecesNotCheckers) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, WHITE_ROOK); // White rook, not checking white king + + // Asking for black attackers, but only white rook exists + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test King on corner with checker. + * @brief Confirms compute_checkers() works with king on corner square. + */ +TEST(ComputeCheckersTest, KingOnCornerWithChecker) { + Board board = Board::Empty(); + board.set_piece(A1, WHITE_KING); + board.set_piece(A8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, A1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(A8)); +} + +/** + * @test King on edge with checker. + * @brief Confirms compute_checkers() works with king on edge square. + */ +TEST(ComputeCheckersTest, KingOnEdgeWithChecker) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(E8)); +} + +/** + * @test Multiple sliders on same line only one checking. + * @brief Confirms compute_checkers() correctly identifies which slider + * is actually checking. + */ +TEST(ComputeCheckersTest, MultipleSlidersSameLine) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, BLACK_ROOK); + board.set_piece(E8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + // Only E4 rook is checking (E8 is blocked by E4) + EXPECT_EQ(checkers.count(), 1); + EXPECT_TRUE(checkers.test(E4)); +} + +/** + * @test Complex position with multiple potential checkers. + * @brief Confirms compute_checkers() correctly identifies all checking + * pieces in complex position. + */ +TEST(ComputeCheckersTest, ComplexPositionMultipleCheckers) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(D2, BLACK_KNIGHT); // Checking + board.set_piece(E8, BLACK_ROOK); // Checking + board.set_piece(A4, BLACK_ROOK); // Checking + board.set_piece(A8, BLACK_BISHOP); // Not checking (blocked) + board.set_piece(C6, BLACK_PAWN); // Blocker + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 3); + EXPECT_TRUE(checkers.test(D2)); + EXPECT_TRUE(checkers.test(E8)); + EXPECT_TRUE(checkers.test(A4)); + EXPECT_FALSE(checkers.test(A8)); +} + +/** + * @test Starting position no checkers. + * @brief Confirms compute_checkers() returns empty for starting position. + */ +TEST(ComputeCheckersTest, StartingPositionNoCheckers) { + Board board = Board::StartingPosition(); + + Bitboard checkers_white = compute_checkers(board, E1, Color::BLACK); + Bitboard checkers_black = compute_checkers(board, E8, Color::WHITE); + + EXPECT_EQ(checkers_white, Bitboard::Zeros()); + EXPECT_EQ(checkers_black, Bitboard::Zeros()); +} + +/** + * @test Slider attack blocked by friendly piece. + * @brief Confirms compute_checkers() does not detect check when friendly + * piece blocks slider. + */ +TEST(ComputeCheckersTest, SliderBlockedByFriendlyPiece) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, BLACK_PAWN); + board.set_piece(E8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test Slider attack blocked by enemy piece. + * @brief Confirms compute_checkers() does not detect check when enemy + * piece (relative to king) blocks slider. + */ +TEST(ComputeCheckersTest, SliderBlockedByEnemyPiece) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, WHITE_PAWN); + board.set_piece(E8, BLACK_ROOK); + + Bitboard checkers = compute_checkers(board, E1, Color::BLACK); + + EXPECT_EQ(checkers, Bitboard::Zeros()); +} + +/** + * @test All piece types as checkers simultaneously. + * @brief Confirms compute_checkers() can detect checks from all piece types. + */ +TEST(ComputeCheckersTest, AllPieceTypesAsCheckers) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KING); + board.set_piece(D2, BLACK_KNIGHT); // Knight check + board.set_piece(D5, BLACK_PAWN); // Pawn check + board.set_piece(E8, BLACK_ROOK); // Rook check + board.set_piece(H7, BLACK_BISHOP); // Bishop check + board.set_piece(H4, BLACK_QUEEN); // Queen check + + Bitboard checkers = compute_checkers(board, E4, Color::BLACK); + + EXPECT_EQ(checkers.count(), 5); + EXPECT_TRUE(checkers.test(D2)); + EXPECT_TRUE(checkers.test(D5)); + EXPECT_TRUE(checkers.test(E8)); + EXPECT_TRUE(checkers.test(H7)); + EXPECT_TRUE(checkers.test(H4)); +} From eb79737a13cdd4362cd7715e92ada9714dfd5ae7 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Mon, 5 Jan 2026 23:46:39 +0100 Subject: [PATCH 7/7] updated legal moves generation accordingly and added some tests --- include/bitbishop/movegen/legal_moves.hpp | 7 +- tests/bitbishop/movegen/test_legal_moves.cpp | 480 +++++++++++++++++++ 2 files changed, 484 insertions(+), 3 deletions(-) create mode 100644 tests/bitbishop/movegen/test_legal_moves.cpp diff --git a/include/bitbishop/movegen/legal_moves.hpp b/include/bitbishop/movegen/legal_moves.hpp index 6c695c1..e9eaf0d 100644 --- a/include/bitbishop/movegen/legal_moves.hpp +++ b/include/bitbishop/movegen/legal_moves.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -40,16 +41,16 @@ * @note The move list is appended to; it is not cleared by this function. * @note Assumes the board position is internally consistent and legal. */ -void generate_legal_moves(std::vector moves, const Board& board, Color us) { +void generate_legal_moves(std::vector& moves, const Board& board, Color us) { Square king_sq = board.king_square(us).value(); Color them = ColorUtil::opposite(us); - Bitboard checkers = attackers_to(king_sq, them); + Bitboard checkers = compute_checkers(board, king_sq, them); 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); - generate_legal_king_moves(moves, board, us, king_sq, enemy_attacks, check_mask); + generate_legal_king_moves(moves, board, us, king_sq, enemy_attacks); generate_castling_moves(moves, board, us, checkers, enemy_attacks); diff --git a/tests/bitbishop/movegen/test_legal_moves.cpp b/tests/bitbishop/movegen/test_legal_moves.cpp new file mode 100644 index 0000000..9dd8854 --- /dev/null +++ b/tests/bitbishop/movegen/test_legal_moves.cpp @@ -0,0 +1,480 @@ +#include + +#include +#include +#include +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +/** + * @test Starting position white moves. + * @brief Confirms generate_legal_moves() generates correct number of moves + * from starting position for white. + */ +TEST(GenerateLegalMovesTest, StartingPositionWhite) { + Board board = Board::StartingPosition(); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // 16 pawn moves (8 single + 8 double) + // 4 knight moves (2 knights * 2 each) + EXPECT_EQ(moves.size(), 20); +} + +/** + * @test Starting position black moves. + * @brief Confirms generate_legal_moves() generates correct number of moves + * from starting position for black. + */ +TEST(GenerateLegalMovesTest, StartingPositionBlack) { + Board board = Board::StartingPosition(); + + std::vector moves; + generate_legal_moves(moves, board, Color::BLACK); + + // 16 pawn moves + 4 knight moves + EXPECT_EQ(moves.size(), 20); +} + +/** + * @test Empty board with only kings. + * @brief Confirms generate_legal_moves() generates only king moves when + * board has only kings. + */ +TEST(GenerateLegalMovesTest, OnlyKingsOnBoard) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, BLACK_KING); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // King has 5 moves (on edge, some squares attacked by black king) + EXPECT_GT(moves.size(), 0); + EXPECT_LT(moves.size(), 8); +} + +/** + * @test King in check must move or block. + * @brief Confirms generate_legal_moves() only generates moves that resolve + * check when king is in single check. + */ +TEST(GenerateLegalMovesTest, KingInSingleCheck) { + Board board("rnb1kbnr/pppp1ppp/8/4p3/6Pq/3P1P2/PPP1P2P/RNBQKBNR b KQkq - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + EXPECT_GT(moves.size(), 0); +} + +/** + * @test King in double check must move. + * @brief Confirms generate_legal_moves() only generates king moves when + * in double check. + */ +TEST(GenerateLegalMovesTest, KingInDoubleCheck) { + Board board("4k3/8/8/8/8/3r4/3r4/4K3 w - - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Only king moves allowed in double check + for (const Move& move : moves) { + EXPECT_EQ(move.from, E1); + } +} + +/** + * @test Pinned pieces generate legal moves. + * @brief Confirms generate_legal_moves() includes moves for pinned pieces + * along pin ray. + */ +TEST(GenerateLegalMovesTest, PinnedPiecesCanMove) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, WHITE_ROOK); + board.set_piece(E8, BLACK_ROOK); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Pinned rook can move along pin ray + EXPECT_TRUE(contains_move(moves, {E4, E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E4, E8, std::nullopt, true, false, false})); + + // Cannot move perpendicular to pin + EXPECT_FALSE(contains_move(moves, {E4, D4, std::nullopt, false, false, false})); +} + +/** + * @test Castling included when legal. + * @brief Confirms generate_legal_moves() includes castling moves when + * all conditions are met. + */ +TEST(GenerateLegalMovesTest, CastlingIncluded) { + Board board("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + EXPECT_TRUE(contains_move(moves, {E1, G1, std::nullopt, false, false, true})); + EXPECT_TRUE(contains_move(moves, {E1, C1, std::nullopt, false, false, true})); +} + +/** + * @test No castling when in check. + * @brief Confirms generate_legal_moves() does not include castling when + * king is in check. + */ +TEST(GenerateLegalMovesTest, NoCastlingWhenInCheck) { + Board board("r3k2r/8/8/8/8/8/4q3/R3K2R w KQkq - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + EXPECT_FALSE(contains_move(moves, {E1, G1, std::nullopt, false, false, true})); + EXPECT_FALSE(contains_move(moves, {E1, C1, std::nullopt, false, false, true})); +} + +/** + * @test All piece types generate moves. + * @brief Confirms generate_legal_moves() includes moves from all piece types. + */ +TEST(GenerateLegalMovesTest, AllPieceTypesGenerate) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(D1, WHITE_KNIGHT); + board.set_piece(C1, WHITE_BISHOP); + board.set_piece(A1, WHITE_ROOK); + board.set_piece(B1, WHITE_QUEEN); + board.set_piece(E8, BLACK_KING); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Should have moves from all piece types + bool has_king_move = false; + bool has_pawn_move = false; + bool has_knight_move = false; + bool has_bishop_move = false; + bool has_rook_move = false; + bool has_queen_move = false; + + for (const Move& move : moves) { + if (move.from == E1) has_king_move = true; + if (move.from == E2) has_pawn_move = true; + if (move.from == D1) has_knight_move = true; + if (move.from == C1) has_bishop_move = true; + if (move.from == A1) has_rook_move = true; + if (move.from == B1) has_queen_move = true; + } + + EXPECT_TRUE(has_king_move); + EXPECT_TRUE(has_pawn_move); + EXPECT_TRUE(has_knight_move); + EXPECT_TRUE(has_bishop_move); + EXPECT_TRUE(has_rook_move); + EXPECT_TRUE(has_queen_move); +} + +/** + * @test Pawn promotions included. + * @brief Confirms generate_legal_moves() includes all pawn promotion moves. + */ +TEST(GenerateLegalMovesTest, PawnPromotionsIncluded) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E7, WHITE_PAWN); + board.set_piece(A8, BLACK_KING); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Should have 4 promotion moves + EXPECT_TRUE(contains_move(moves, {E7, E8, WHITE_QUEEN, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E7, E8, WHITE_ROOK, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E7, E8, WHITE_BISHOP, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E7, E8, WHITE_KNIGHT, false, false, false})); +} + +/** + * @test En passant included. + * @brief Confirms generate_legal_moves() includes en passant captures. + */ +TEST(GenerateLegalMovesTest, EnPassantIncluded) { + Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); +} + +/** + * @test Captures included. + * @brief Confirms generate_legal_moves() includes capture moves. + */ +TEST(GenerateLegalMovesTest, CapturesIncluded) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, WHITE_ROOK); + board.set_piece(E7, BLACK_PAWN); + board.set_piece(E8, BLACK_KING); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + EXPECT_TRUE(contains_move(moves, {E4, E7, std::nullopt, true, false, false})); +} + +/** + * @test No illegal moves generated. + * @brief Confirms generate_legal_moves() does not generate moves that + * would leave king in check. + */ +TEST(GenerateLegalMovesTest, NoIllegalMoves) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_QUEEN); + board.set_piece(E8, BLACK_ROOK); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // White queen cannot move east or west as it would expose the king + EXPECT_FALSE(contains_move(moves, {E2, D2, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {E2, F2, std::nullopt, false, false, false})); + + // White queen can move towards the black rook + EXPECT_TRUE(contains_move(moves, {E2, E3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E8, std::nullopt, true, false, false})); +} + +/** + * @test Moves vector not cleared. + * @brief Confirms generate_legal_moves() appends to existing moves vector. + */ +TEST(GenerateLegalMovesTest, MovesVectorNotCleared) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E8, BLACK_KING); + + std::vector moves; + moves.emplace_back(A1, A2, std::nullopt, false, false, false); + + size_t initial_size = moves.size(); + generate_legal_moves(moves, board, Color::WHITE); + + EXPECT_GT(moves.size(), initial_size); + EXPECT_TRUE(contains_move(moves, {A1, A2, std::nullopt, false, false, false})); +} + +/** + * @test Stalemate position generates no moves. + * @brief Confirms generate_legal_moves() generates no moves in stalemate. + */ +TEST(GenerateLegalMovesTest, StalemateNoMoves) { + Board board("7k/5Q2/6K1/8/8/8/8/8 b - - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Checkmate position only has king moves (all illegal). + * @brief Confirms generate_legal_moves() handles checkmate positions. + */ +TEST(GenerateLegalMovesTest, CheckmatePosition) { + Board board("8/8/8/8/8/8/1r6/r2K4 w - - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // King in checkmate - no legal moves + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Back rank mate threat. + * @brief Confirms generate_legal_moves() correctly handles back rank + * mate scenarios. + */ +TEST(GenerateLegalMovesTest, BackRankMateThreat) { + Board board("6k1/5ppp/8/8/8/8/5PPP/5RK1 w - - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Should generate moves but not illegal king moves + EXPECT_GT(moves.size(), 0); +} + +/** + * @test Discovered check pieces restricted. + * @brief Confirms generate_legal_moves() correctly handles pieces that + * would create discovered check if moved. + */ +TEST(GenerateLegalMovesTest, DiscoveredCheckRestriction) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, WHITE_BISHOP); + board.set_piece(E8, BLACK_ROOK); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Bishop pinned, cannot move + for (const Move& move : moves) { + EXPECT_NE(move.from, E4); + } +} + +/** + * @test Complex position with multiple piece types. + * @brief Confirms generate_legal_moves() handles complex positions correctly. + */ +TEST(GenerateLegalMovesTest, ComplexPosition) { + Board board("r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Should have many legal moves + EXPECT_GT(moves.size(), 20); +} + +/** + * @test Only king moves in double check. + * @brief Confirms generate_legal_moves() only generates king moves when + * in double check. + */ +TEST(GenerateLegalMovesTest, OnlyKingMovesInDoubleCheck) { + Board board("4k3/8/8/8/8/2q5/2r5/4K3 w - - 0 1"); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // All moves should be from the king + for (const Move& move : moves) { + EXPECT_EQ(move.from, E1); + } +} + +/** + * @test Blocking moves included in single check. + * @brief Confirms generate_legal_moves() includes moves that block check. + */ +TEST(GenerateLegalMovesTest, BlockingMovesIncluded) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(D2, WHITE_BISHOP); + board.set_piece(E8, BLACK_ROOK); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Bishop can block on E file + bool has_blocking_move = false; + for (const Move& move : moves) { + if (move.from == D2 && (move.to == E3 || move.to == E4 || move.to == E5 || move.to == E6 || move.to == E7)) { + has_blocking_move = true; + } + } + EXPECT_TRUE(has_blocking_move); +} + +/** + * @test Capturing checker included. + * @brief Confirms generate_legal_moves() includes moves that capture + * the checking piece. + */ +TEST(GenerateLegalMovesTest, CapturingCheckerIncluded) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(D2, WHITE_KNIGHT); + board.set_piece(E5, BLACK_QUEEN); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Should include capturing the queen (if knight can reach it) + // Or king moving away + EXPECT_GT(moves.size(), 0); +} + +/** + * @test All moves from starting position are unique. + * @brief Confirms generate_legal_moves() doesn't generate duplicate moves. + */ +TEST(GenerateLegalMovesTest, NoDuplicateMoves) { + Board board = Board::StartingPosition(); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Check for duplicates + for (size_t i = 0; i < moves.size(); i++) { + for (size_t j = i + 1; j < moves.size(); j++) { + bool same = (moves[i].from == moves[j].from && moves[i].to == moves[j].to && + moves[i].promotion == moves[j].promotion && moves[i].is_capture == moves[j].is_capture && + moves[i].is_en_passant == moves[j].is_en_passant && moves[i].is_castling == moves[j].is_castling); + EXPECT_FALSE(same) << "Duplicate move found"; + } + } +} + +/** + * @test Knight moves included when not pinned. + * @brief Confirms generate_legal_moves() includes knight moves when legal. + */ +TEST(GenerateLegalMovesTest, KnightMovesIncluded) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(D4, WHITE_KNIGHT); + board.set_piece(E8, BLACK_KING); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Knight should have moves + bool has_knight_move = false; + for (const Move& move : moves) { + if (move.from == D4) { + has_knight_move = true; + break; + } + } + EXPECT_TRUE(has_knight_move); +} + +/** + * @test Pinned knight generates no moves. + * @brief Confirms generate_legal_moves() excludes pinned knight moves. + */ +TEST(GenerateLegalMovesTest, PinnedKnightNoMoves) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E3, WHITE_KNIGHT); + board.set_piece(E8, BLACK_ROOK); + + std::vector moves; + generate_legal_moves(moves, board, Color::WHITE); + + // Pinned knight cannot move + for (const Move& move : moves) { + EXPECT_NE(move.from, E3); + } +}