diff --git a/include/bitbishop/board.hpp b/include/bitbishop/board.hpp index 48b6477..ab386f4 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -26,8 +26,8 @@ class Board { private: // Piece bitboards - Bitboard m_w_pawns, m_w_rooks, m_w_bishops, m_w_knights, m_w_king, m_w_queen; - Bitboard m_b_pawns, m_b_rooks, m_b_bishops, m_b_knights, m_b_king, m_b_queen; + Bitboard m_w_pawns, m_w_rooks, m_w_bishops, m_w_knights, m_w_king, m_w_queens; + Bitboard m_b_pawns, m_b_rooks, m_b_bishops, m_b_knights, m_b_king, m_b_queens; // Game state bool m_is_white_turn; ///< True if it is White's turn @@ -163,6 +163,14 @@ class Board { */ [[nodiscard]] Bitboard bishops(Color side) const { return (side == Color::WHITE) ? m_w_bishops : m_b_bishops; } + /** + * @brief Returns a bitboard representing the queen(s) belonging to the given side. + * + * @param side The color corresponding to the side to move (Color::WHITE or Color::BLACK). + * @return Bitboard containing all squares occupied by that side's queen(s). + */ + [[nodiscard]] Bitboard queens(Color side) const { return (side == Color::WHITE) ? m_w_queens : m_b_queens; } + /** * @brief Returns a bitboard of all enemy pieces relative to the given side to move. * diff --git a/include/bitbishop/lookups/queen.hpp b/include/bitbishop/lookups/queen.hpp index f64bb14..736f9d1 100644 --- a/include/bitbishop/lookups/queen.hpp +++ b/include/bitbishop/lookups/queen.hpp @@ -7,6 +7,10 @@ #include #include +// NOTE: These queen lookups are not used in moves generation code. +// Instead, rook and bishop rays are used individually and reused to generate queen rays, +// as it is the same logic. + namespace Lookups { /** diff --git a/include/bitbishop/moves/queen_move_gen.hpp b/include/bitbishop/moves/queen_move_gen.hpp new file mode 100644 index 0000000..7cec501 --- /dev/null +++ b/include/bitbishop/moves/queen_move_gen.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include + +/** + * @brief Generates queen moves. + * + * Queen can move: + * + * - diagonally in the four directions (NE, NW, SE, SW) + * - horizontally (E, W) + * - vertically (N, S) + * + * until a friendly or enemy piece is encountered. + * + * This namespace provides functions for generating both pseudo-legal and legal queen moves. + */ +namespace QueenMoveGenerator { + +/** + * @brief Generates all pseudo-legal queen moves for a given side. + * + * Pseudo-legal moves follow piece movement rules but may leave the king in check. + * These moves must be validated separately to ensure legality. + * + * @param moves Vector to append generated moves to + * @param board Current board state + * @param side Color of the side to generate moves for + */ +void generate_pseudo_legal_moves(std::vector& moves, const Board& board, Color side); + +}; // namespace QueenMoveGenerator diff --git a/src/bitbishop/board.cpp b/src/bitbishop/board.cpp index e45f06e..923ffa1 100644 --- a/src/bitbishop/board.cpp +++ b/src/bitbishop/board.cpp @@ -77,7 +77,7 @@ Bitboard Board::white_pieces() const { bitboard |= m_w_bishops; bitboard |= m_w_knights; bitboard |= m_w_king; - bitboard |= m_w_queen; + bitboard |= m_w_queens; return bitboard; } @@ -88,7 +88,7 @@ Bitboard Board::black_pieces() const { bitboard |= m_b_bishops; bitboard |= m_b_knights; bitboard |= m_b_king; - bitboard |= m_b_queen; + bitboard |= m_b_queens; return bitboard; } @@ -99,13 +99,13 @@ Bitboard Board::occupied() const { bitboard |= m_b_bishops; bitboard |= m_b_knights; bitboard |= m_b_king; - bitboard |= m_b_queen; + bitboard |= m_b_queens; bitboard |= m_w_pawns; bitboard |= m_w_rooks; bitboard |= m_w_bishops; bitboard |= m_w_knights; bitboard |= m_w_king; - bitboard |= m_w_queen; + bitboard |= m_w_queens; return bitboard; } @@ -115,14 +115,14 @@ std::optional Board::get_piece(Square square) const { if (m_w_knights.test(square)) { return Pieces::WHITE_KNIGHT; } if (m_w_bishops.test(square)) { return Pieces::WHITE_BISHOP; } if (m_w_rooks.test(square)) { return Pieces::WHITE_ROOK; } - if (m_w_queen.test(square)) { return Pieces::WHITE_QUEEN; } + if (m_w_queens.test(square)) { return Pieces::WHITE_QUEEN; } if (m_w_king.test(square)) { return Pieces::WHITE_KING; } if (m_b_pawns.test(square)) { return Pieces::BLACK_PAWN; } if (m_b_knights.test(square)) { return Pieces::BLACK_KNIGHT; } if (m_b_bishops.test(square)) { return Pieces::BLACK_BISHOP; } if (m_b_rooks.test(square)) { return Pieces::BLACK_ROOK; } - if (m_b_queen.test(square)) { return Pieces::BLACK_QUEEN; } + if (m_b_queens.test(square)) { return Pieces::BLACK_QUEEN; } if (m_b_king.test(square)) { return Pieces::BLACK_KING; } // clang-format on @@ -144,7 +144,7 @@ void Board::set_piece(Square square, Piece piece) { case Piece::KNIGHT: m_w_knights.set(square); return; case Piece::BISHOP: m_w_bishops.set(square); return; case Piece::ROOK: m_w_rooks.set(square); return; - case Piece::QUEEN: m_w_queen.set(square); return; + case Piece::QUEEN: m_w_queens.set(square); return; case Piece::KING: m_w_king.set(square); return; default: break; // clang-format on } @@ -155,7 +155,7 @@ void Board::set_piece(Square square, Piece piece) { case Piece::KNIGHT: m_b_knights.set(square); return; case Piece::BISHOP: m_b_bishops.set(square); return; case Piece::ROOK: m_b_rooks.set(square); return; - case Piece::QUEEN: m_b_queen.set(square); return; + case Piece::QUEEN: m_b_queens.set(square); return; case Piece::KING: m_b_king.set(square); return; default: break; // clang-format on } @@ -172,14 +172,14 @@ void Board::remove_piece(Square square) { m_w_knights.clear(square); m_w_bishops.clear(square); m_w_rooks.clear(square); - m_w_queen.clear(square); + m_w_queens.clear(square); m_w_king.clear(square); m_b_pawns.clear(square); m_b_knights.clear(square); m_b_bishops.clear(square); m_b_rooks.clear(square); - m_b_queen.clear(square); + m_b_queens.clear(square); m_b_king.clear(square); } diff --git a/src/bitbishop/moves/queen_move_gen.cpp b/src/bitbishop/moves/queen_move_gen.cpp new file mode 100644 index 0000000..596d47f --- /dev/null +++ b/src/bitbishop/moves/queen_move_gen.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include + +void QueenMoveGenerator::generate_pseudo_legal_moves(std::vector& moves, const Board& board, Color side) { + // Multiple queens may be present on the board after pawn promotion + Bitboard queens = board.queens(side); + Bitboard empty = board.unoccupied(); + Bitboard enemy = board.enemy(side); + Bitboard occupied = board.occupied(); + + // warning: this loop is destructive on Bitboard queens + while (auto from_opt = queens.pop_lsb()) { + Square from = from_opt.value(); + + Bitboard queen_attacks; + queen_attacks |= RookMoveGenerator::north_ray(from, occupied); + queen_attacks |= RookMoveGenerator::south_ray(from, occupied); + queen_attacks |= RookMoveGenerator::east_ray(from, occupied); + queen_attacks |= RookMoveGenerator::west_ray(from, occupied); + queen_attacks |= BishopMoveGenerator::north_east_ray(from, occupied); + queen_attacks |= BishopMoveGenerator::north_west_ray(from, occupied); + queen_attacks |= BishopMoveGenerator::south_east_ray(from, occupied); + queen_attacks |= BishopMoveGenerator::south_west_ray(from, occupied); + + // Queen sliding moves + Bitboard queen_moves = queen_attacks & empty; + for (auto to : queen_moves) { + moves.emplace_back(from, to, std::nullopt, false, false, false); + } + + // Queen captures + Bitboard queen_captures = queen_attacks & enemy; + for (auto to : queen_captures) { + moves.emplace_back(from, to, std::nullopt, true, false, false); + } + } +} diff --git a/tests/bitbishop/board/test_b_get_piece_bitboard.cpp b/tests/bitbishop/board/test_b_get_piece_bitboard.cpp index 3994128..30ee5e5 100644 --- a/tests/bitbishop/board/test_b_get_piece_bitboard.cpp +++ b/tests/bitbishop/board/test_b_get_piece_bitboard.cpp @@ -91,3 +91,19 @@ TEST(BoardTest, BishopsBitboard) { EXPECT_TRUE(black_bishops.test(Square::C8)); EXPECT_TRUE(black_bishops.test(Square::F8)); } + +/** + * @test BoardTest.QueensBitboard + * @brief Ensures that queens() correctly returns the bitboard for each side. + */ +TEST(BoardTest, QueensBitboard) { + Board board; + + Bitboard white_queens = board.queens(Color::WHITE); + Bitboard black_queens = board.queens(Color::BLACK); + + EXPECT_EQ(white_queens.count(), 1); + EXPECT_EQ(black_queens.count(), 1); + EXPECT_TRUE(white_queens.test(Squares::D1)); + EXPECT_TRUE(black_queens.test(Square::D8)); +} diff --git a/tests/bitbishop/moves/queen_move_generator/test_qmg_generate_pseudo_legal_moves.cpp b/tests/bitbishop/moves/queen_move_generator/test_qmg_generate_pseudo_legal_moves.cpp new file mode 100644 index 0000000..5755b43 --- /dev/null +++ b/tests/bitbishop/moves/queen_move_generator/test_qmg_generate_pseudo_legal_moves.cpp @@ -0,0 +1,525 @@ +#include + +#include +#include +#include + +/** + * @brief Test fixture for queen pseudo-legal move generation. + */ +class QueenPseudoLegalMovesTest : public ::testing::Test { + protected: + std::vector moves; + + void SetUp() override {} + + void TearDown() override {} +}; + +/** + * @test White queen has no moves from starting position + */ +TEST_F(QueenPseudoLegalMovesTest, StartingPositionWhiteHas0Moves) { + Board board; + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Black queen has no moves from starting position + */ +TEST_F(QueenPseudoLegalMovesTest, StartingPositionBlackHas0Moves) { + Board board; + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test White queen in the center has 27 regular moves (8 directions) + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenCenterEmptyBoardHas27Moves) { + Board board("8/8/8/8/3Q4/8/8/8 w - - 0 1"); // White queen on D4 + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 27); + EXPECT_EQ(count_captures(moves), 0); + + // North Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D8, std::nullopt, false, false, false})); + + // South Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D1, std::nullopt, false, false, false})); + + // East Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::G4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::H4, std::nullopt, false, false, false})); + + // West Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A4, std::nullopt, false, false, false})); + + // North East Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::G7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::H8, std::nullopt, false, false, false})); + + // North West Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A7, std::nullopt, false, false, false})); + + // South East Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::G1, std::nullopt, false, false, false})); + + // South West Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A1, std::nullopt, false, false, false})); +} + +/** + * @test Black queen in the center has 27 regular moves (8 directions) + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenCenterEmptyBoardHas27Moves) { + Board board("8/8/8/8/3q4/8/8/8 b - - 0 1"); // Black queen on D4 + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 27); + EXPECT_EQ(count_captures(moves), 0); + + // North Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D8, std::nullopt, false, false, false})); + + // South Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D1, std::nullopt, false, false, false})); + + // East Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::G4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::H4, std::nullopt, false, false, false})); + + // West Ray (rook-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A4, std::nullopt, false, false, false})); + + // North East Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::G7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::H8, std::nullopt, false, false, false})); + + // North West Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A7, std::nullopt, false, false, false})); + + // South East Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::G1, std::nullopt, false, false, false})); + + // South West Ray (bishop-like) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A1, std::nullopt, false, false, false})); +} + +/** + * @test White queen in corner has 21 regular moves + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenCornerHas21Moves) { + Board board("7Q/8/8/8/8/8/8/8 w - - 0 1"); // White queen on H8 + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 21); + EXPECT_EQ(count_captures(moves), 0); + + // South Ray + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::H7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::H6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::H5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::H4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::H3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::H2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::H1, std::nullopt, false, false, false})); + + // West Ray + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::G8, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::F8, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::E8, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::D8, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::C8, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::B8, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::A8, std::nullopt, false, false, false})); + + // South West Ray (diagonal) + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::G7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::F6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::D4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::C3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::B2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::H8, Squares::A1, std::nullopt, false, false, false})); +} + +/** + * @test Black queen in corner has 21 regular moves + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenCornerHas21Moves) { + Board board("8/8/8/8/8/8/8/q7 b - - 0 1"); // Black queen on A1 + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 21); + EXPECT_EQ(count_captures(moves), 0); + + // North Ray + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A8, std::nullopt, false, false, false})); + + // East Ray + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::B1, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::C1, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::D1, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::E1, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::F1, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::G1, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::H1, std::nullopt, false, false, false})); + + // North East Ray (diagonal) + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::B2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::C3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::D4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::F6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::G7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::H8, std::nullopt, false, false, false})); +} + +/** + * @test White queen on edge has 21 regular moves + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenEdgeHas21Moves) { + Board board("8/8/8/8/7Q/8/8/8 w - - 0 1"); // White queen on H4 + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 21); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Black queen on edge has 21 regular moves + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenEdgeHas21Moves) { + Board board("8/8/8/8/q7/8/8/8 b - - 0 1"); // Black queen on A4 + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 21); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test White queen can capture enemy pieces and stops at capture + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenCanCaptureEnemyPieces) { + // White queen on D4 with black pawns on D6 (north), F4 (east), F6 (NE), B2 (SW) + Board board("8/8/3p1p2/8/3Q1p2/8/1p6/8 w - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_captures(moves), 4); + + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B2, std::nullopt, true, false, false})); + + // Should not go beyond captures + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::D7, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::G4, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::G7, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::A1, std::nullopt, false, false, false})); +} + +/** + * @test Black queen can capture enemy pieces and stops at capture + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenCanCaptureEnemyPieces) { + // Black queen on D4 with white pawns on D6 (north), F4 (east), F6 (NE), B2 (SW) + Board board("8/8/3P1P2/8/3q1P2/8/1P6/8 b - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_captures(moves), 4); + + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B2, std::nullopt, true, false, false})); +} + +/** + * @test White queen cannot capture own pieces and stops before them + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenCannotCaptureOwnPieces) { + // White queen on D4 with white pawns on D6 (north), F4 (east), F6 (NE), B2 (SW) + Board board("8/8/3P1P2/8/3Q1P2/8/1P6/8 w - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_captures(moves), 0); + + // North Ray - should stop before D6 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D5, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::D7, std::nullopt, false, false, false})); + + // East Ray - should stop before F4 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E4, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::G4, std::nullopt, false, false, false})); + + // North East Ray - should stop before F6 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::G7, std::nullopt, false, false, false})); + + // South West Ray - should stop before B2 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::B2, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::A1, std::nullopt, false, false, false})); +} + +/** + * @test Black queen cannot capture own pieces and stops before them + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenCannotCaptureOwnPieces) { + // Black queen on D4 with black pawns on D6 (north), F4 (east), F6 (NE), B2 (SW) + Board board("8/8/3p1p2/8/3q1p2/8/1p6/8 b - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_captures(moves), 0); + + // North Ray - should stop before D6 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D5, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::D7, std::nullopt, false, false, false})); + + // East Ray - should stop before F4 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E4, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::G4, std::nullopt, false, false, false})); + + // North East Ray - should stop before F6 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::G7, std::nullopt, false, false, false})); + + // South West Ray - should stop before B2 (own piece) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::B2, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::A1, std::nullopt, false, false, false})); +} + +/** + * @test White queen with mixed occupancy (own and enemy pieces on different rays) + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenMixedOccupancy) { + // White queen on D4 + // White knight on D6 (N), black rook on F4 (E) + // White pawn on C3 (SW), black queen on E5 (NE) + Board board("8/8/3N4/4q3/3Q1r2/2P5/8/8 w - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_captures(moves), 2); + + // Can capture enemy pieces + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, true, false, false})); + + // Cannot capture own pieces + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, false, false, false})); +} + +/** + * @test Black queen with mixed occupancy (own and enemy pieces on different rays) + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenMixedOccupancy) { + // Black queen on D4 + // Black knight on D6 (N), white rook on F4 (E) + // Black pawn on C3 (SW), white queen on E5 (NE) + Board board("8/8/3n4/4Q3/3q1R2/2p5/8/8 b - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_captures(moves), 2); + + // Can capture enemy pieces + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, true, false, false})); + + // Cannot capture own pieces + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::D6, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, false, false, false})); +} + +/** + * @test Multiple white queens on the board (after promotion) + */ +TEST_F(QueenPseudoLegalMovesTest, MultipleWhiteQueens) { + // Two white queens on D4 and E5 + Board board("8/8/8/4Q3/3Q4/8/8/8 w - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + // Each queen should have moves, though some rays will be blocked by the other queen + EXPECT_GT(moves.size(), 0); + + // D4 queen can move to C4, B4, A4 (west ray unblocked) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A4, std::nullopt, false, false, false})); + + // E5 queen can move to F5, G5, H5 (east ray unblocked) + EXPECT_TRUE(contains_move(moves, {Squares::E5, Squares::F5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::E5, Squares::G5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::E5, Squares::H5, std::nullopt, false, false, false})); + + // D4 queen cannot move through or capture E5 queen (friendly) + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, false, false, false})); +} + +/** + * @test Multiple black queens on the board (after promotion) + */ +TEST_F(QueenPseudoLegalMovesTest, MultipleBlackQueens) { + // Two black queens on D4 and E5 + Board board("8/8/8/4q3/3q4/8/8/8 b - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + // Each queen should have moves, though some rays will be blocked by the other queen + EXPECT_GT(moves.size(), 0); + + // D4 queen can move to C4, B4, A4 (west ray unblocked) + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::A4, std::nullopt, false, false, false})); + + // E5 queen can move to F5, G5, H5 (east ray unblocked) + EXPECT_TRUE(contains_move(moves, {Squares::E5, Squares::F5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::E5, Squares::G5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::E5, Squares::H5, std::nullopt, false, false, false})); + + // D4 queen cannot move through or capture E5 queen (friendly) + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {Squares::D4, Squares::F6, std::nullopt, false, false, false})); +} + +/** + * @test White queen fully surrounded by own pieces has no moves + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenFullySurrounded) { + // White queen on D4 completely surrounded by white pawns + Board board("8/8/8/2PPP3/2PQP3/2PPP3/8/8 w - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Black queen fully surrounded by own pieces has no moves + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenFullySurrounded) { + // Black queen on D4 completely surrounded by black pawns + Board board("8/8/8/2ppp3/2pqp3/2ppp3/8/8 b - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test White queen can capture all surrounding enemy pieces + */ +TEST_F(QueenPseudoLegalMovesTest, WhiteQueenCanCaptureAllSurrounding) { + // White queen on D4 completely surrounded by black pawns + Board board("8/8/8/2ppp3/2pQp3/2ppp3/8/8 w - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 8); + EXPECT_EQ(count_captures(moves), 8); + + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D5, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E3, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D3, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C5, std::nullopt, true, false, false})); +} + +/** + * @test Black queen can capture all surrounding enemy pieces + */ +TEST_F(QueenPseudoLegalMovesTest, BlackQueenCanCaptureAllSurrounding) { + // Black queen on D4 completely surrounded by white pawns + Board board("8/8/8/2PPP3/2PqP3/2PPP3/8/8 b - - 0 1"); + + QueenMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 8); + EXPECT_EQ(count_captures(moves), 8); + + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D5, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E5, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::E3, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::D3, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C3, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::C5, std::nullopt, true, false, false})); +}