diff --git a/include/bitbishop/board.hpp b/include/bitbishop/board.hpp index 14ce692..c10795e 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -56,6 +56,8 @@ class Board { */ Board(); + Board(const Board&) = default; + /** * @brief Constructs a board from a FEN string. * @param fen Forsyth–Edwards Notation describing a chess position. @@ -77,6 +79,19 @@ class Board { */ [[nodiscard]] std::optional get_piece(Square square) const; + /** + * @brief Moves a piece from one square to another. + * + * The function will move the piece of any type/color from `from` to `to`. + * If a piece already exists on `to`, it is removed (captured). + * If no piece exists on `from`, sinlently returns. + * If `from` and `to` are identical, silently returns. + * + * @param from Source square + * @param to Destination square + */ + void move_piece(Square from, Square to); + /** * @brief (Re)Places a piece on a given square. * @param square The square where the piece will be placed. @@ -130,6 +145,18 @@ class Board { */ [[nodiscard]] Bitboard unoccupied() const { return ~occupied(); } + /** + * @brief Returns the square of the king for the given color. + * + * In standard chess, a king is always present. However, for tests, variants, + * or incomplete board setups, the king may be missing. To handle this safely, + * the result is returned as a std::optional. + * + * @param us Color of the king + * @return std::optional containing the king's square if present, std::nullopt otherwise + */ + [[nodiscard]] std::optional king_square(Color us) const { return king(us).pop_lsb(); } + /** * @brief Returns a bitboard representing all pawns belonging to the given side to move. * @@ -254,4 +281,27 @@ class Board { * @return true if queenside castling is legal, false otherwise */ [[nodiscard]] bool can_castle_queenside(Color side) const noexcept; -}; \ No newline at end of file + + Board& operator=(const Board& other) = default; + + /** + * @brief Checks if two boards represent the same chess position. + * + * Compares piece placement, side to move, en passant square, and castling rights. + * Ignores half-move clock and full-move number. + * + * @param other The board to compare against + * @return true if positions are identical, false otherwise + */ + [[nodiscard]] bool operator==(const Board& other) const; + + /** + * @brief Checks if two boards represent different positions. + * + * Logical negation of operator==(). + * + * @param other The board to compare against + * @return true if positions differ, false otherwise + */ + [[nodiscard]] bool operator!=(const Board& other) const; +}; diff --git a/include/bitbishop/lookups/pawn_attacks.hpp b/include/bitbishop/lookups/pawn_attacks.hpp index 6784aec..1ba5999 100644 --- a/include/bitbishop/lookups/pawn_attacks.hpp +++ b/include/bitbishop/lookups/pawn_attacks.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -167,7 +168,7 @@ constexpr std::array BLACK_PAWN_ATTACKS = []() cons * * Indexed by target square (0–63). */ -constexpr std::array WHITE_PAWN_ATTACKERS = BLACK_PAWN_ATTACKS; +constexpr std::array WHITE_PAWN_ATTACKERS = BLACK_PAWN_ATTACKS; /** * @brief Precomputed bitboards of BLACK pawn attackers (reverse pawn attacks). @@ -186,6 +187,112 @@ constexpr std::array WHITE_PAWN_ATTACKERS = BLACK_PAWN_ATTACKS; * * Indexed by target square (0–63). */ -constexpr std::array BLACK_PAWN_ATTACKERS = WHITE_PAWN_ATTACKS; +constexpr std::array BLACK_PAWN_ATTACKERS = WHITE_PAWN_ATTACKS; + +/** + * @brief Precomputed single-push destinations for pawns of both colors. + * + * For each color and source square, contains a bitboard with the destination + * square if a pawn moves one square forward: + * - White pawns move north (+1 rank) + * - Black pawns move south (-1 rank) + * + * This table represents theoretical move destinations only. + * The caller must ensure: + * - The destination square is empty + * - The move satisfies check and pin constraints + * + * Indexed as [color][source_square]. + */ +constexpr std::array, ColorUtil::size()> PAWN_SINGLE_PUSH = []() constexpr { + using namespace Const; + + std::array, ColorUtil::size()> table{}; + + for (Color color : ColorUtil::all()) { + const auto idx = ColorUtil::to_index(color); + switch (color) { + case Color::WHITE: + table[idx] = WHITE_PAWN_SINGLE_PUSH; + break; + case Color::BLACK: + table[idx] = BLACK_PAWN_SINGLE_PUSH; + break; + } + } + + return table; +}(); + +/** + * @brief Precomputed double-push destinations for pawns of both colors. + * + * For each color and source square, contains a bitboard with the destination + * square if a pawn moves two squares forward from its starting rank: + * - White: rank 2 → rank 4 + * - Black: rank 7 → rank 5 + * + * This table represents theoretical move destinations only. + * The caller must ensure: + * - The pawn is on its starting rank + * - Both the intermediate and destination squares are empty + * - The move satisfies check and pin constraints + * + * Indexed as [color][source_square]. + */ +constexpr std::array, ColorUtil::size()> PAWN_DOUBLE_PUSH = []() constexpr { + using namespace Const; + + std::array, ColorUtil::size()> table{}; + + for (Color color : ColorUtil::all()) { + const auto idx = ColorUtil::to_index(color); + switch (color) { + case Color::WHITE: + table[idx] = WHITE_PAWN_DOUBLE_PUSH; + break; + case Color::BLACK: + table[idx] = BLACK_PAWN_DOUBLE_PUSH; + break; + } + } + + return table; +}(); + +/** + * @brief Precomputed diagonal capture targets for pawns of both colors. + * + * For each color and source square, contains a bitboard with all squares + * a pawn could capture to diagonally: + * - White: north-west and north-east + * - Black: south-west and south-east + * + * This table is independent of board occupancy. + * The caller must verify that: + * - An enemy piece occupies the target square, or + * - The square corresponds to a valid en passant target + * + * Indexed as [color][source_square]. + */ +constexpr std::array, ColorUtil::size()> PAWN_ATTACKS = []() constexpr { + using namespace Const; + + std::array, ColorUtil::size()> table{}; + + for (Color color : ColorUtil::all()) { + const auto idx = ColorUtil::to_index(color); + switch (color) { + case Color::WHITE: + table[idx] = WHITE_PAWN_ATTACKS; + break; + case Color::BLACK: + table[idx] = BLACK_PAWN_ATTACKS; + break; + } + } + + return table; +}(); } // namespace Lookups \ No newline at end of file diff --git a/include/bitbishop/movegen/pawn_moves.hpp b/include/bitbishop/movegen/pawn_moves.hpp new file mode 100644 index 0000000..fa5f251 --- /dev/null +++ b/include/bitbishop/movegen/pawn_moves.hpp @@ -0,0 +1,278 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr std::array WHITE_PROMOTIONS = {Pieces::WHITE_QUEEN, Pieces::WHITE_ROOK, Pieces::WHITE_BISHOP, + Pieces::WHITE_KNIGHT}; +constexpr std::array BLACK_PROMOTIONS = {Pieces::BLACK_QUEEN, Pieces::BLACK_ROOK, Pieces::BLACK_BISHOP, + Pieces::BLACK_KNIGHT}; + +/** + * @brief Checks if a square is on the pawn's starting rank. + * + * Pawns start on rank 2 (index 1) for White and rank 7 (index 6) for Black. + * This is used to determine if a pawn is eligible to perform a double push. + * + * @param square Square to check + * @param color Color of the pawn + * @return true if the square is on the starting rank for the given color, false otherwise + */ +constexpr bool is_starting_rank(Square square, Color color) { + using namespace Const; + return color == Color::WHITE ? (square.rank() == RANK_2_IND) : (square.rank() == RANK_7_IND); +} + +/** + * @brief Checks if a square is on the pawn's promotion rank. + * + * Pawns promote when reaching rank 8 (index 7) for White or rank 1 (index 0) for Black. + * + * @param square Square to check + * @param color Color of the pawn + * @return true if the square is on the promotion rank for the given color, false otherwise + */ +constexpr bool is_promotion_rank(Square square, Color color) { + using namespace Const; + return color == Color::WHITE ? (square.rank() == RANK_8_IND) : (square.rank() == RANK_1_IND); +} + +/** + * @brief Validates if an en passant capture is geometrically legal. + * + * En passant geometry is valid when: + * - The capturing pawn is on rank 5 for White (index 4) or rank 4 for Black (index 3) + * - The en passant target square is exactly one file adjacent (horizontally) + * - The en passant target square is on rank 6 for White (index 5) or rank 3 for Black (index 2) + * + * Note: This only checks geometric validity. The caller must separately verify that + * en passant is actually available (i.e., an enemy pawn just performed a double push + * to the square behind the target square). + * + * @param from Square of the capturing pawn + * @param epsq Available en passant target square + * @param side Color of the capturing pawn + * @return true if the capture geometry is valid for en passant, false otherwise + */ +constexpr bool can_capture_en_passant(Square from, Square epsq, Color side) noexcept { + using namespace Const; + + // MSVC have not yet made std::abs() constexpr for C++ 23, forcing us to define a generic constexpr one... + // For this, lets apply the abs() function manually. This is sad, but you know, MSVC... + // The code should not adapt to the compiler for the same language, the compiler should... + int dif = from.file() - epsq.file(); + dif = (dif < 0) ? -dif : dif; + + if (dif != 1) { + return false; + } + if (side == Color::WHITE && from.rank() == RANK_5_IND && epsq.rank() == RANK_6_IND) { + return true; + } + if (side == Color::BLACK && from.rank() == RANK_4_IND && epsq.rank() == RANK_3_IND) { + return true; + } + return false; +} + +/** + * @brief Adds all four promotion moves (Queen, Rook, Bishop, Knight) to the move list. + * + * When a pawn reaches the opposite end of the board (rank 8 for White, rank 1 for Black), + * it must promote to another piece. This function adds all four possible promotions + * for a single pawn move. + * + * @param moves Vector to append promotion moves to + * @param from Source square of the promoting pawn + * @param to Destination square (must be on the promotion rank) + * @param side Color of the promoting pawn + * @param is_capture Whether the promotion involves capturing an enemy piece + */ +void add_pawn_promotions(std::vector& moves, Square from, Square to, Color side, bool capture) { + const auto& promotion_pieces = (side == Color::WHITE) ? WHITE_PROMOTIONS : BLACK_PROMOTIONS; + + for (auto piece : promotion_pieces) { + moves.emplace_back(from, to, piece, capture, false, false); + } +} + +/** + * @brief Generates all single-square pawn pushes from the given square. + * + * Applies occupancy, check mask, and pin mask filters. If the target square + * is a promotion rank, all promotion moves are added. + * + * @param moves Vector to append generated moves to + * @param from Source square of the pawn + * @param us Color of the pawn + * @param occupied Bitboard of all occupied squares + * @param check_mask Bitboard mask to restrict moves under check + * @param pin_mask Bitboard mask to restrict moves due to pins + */ +inline void generate_single_push(std::vector& moves, Square from, Color us, const Bitboard& occupied, + const Bitboard& check_mask, const Bitboard& pin_mask) { + const auto& single_push = Lookups::PAWN_SINGLE_PUSH[ColorUtil::to_index(us)]; + + Bitboard bb = single_push[from.flat_index()]; + bb &= ~occupied; + bb &= check_mask; + bb &= pin_mask; + + if (bb) { + Square to = bb.pop_lsb().value(); + if (is_promotion_rank(to, us)) { + add_pawn_promotions(moves, from, to, us, false); + } else { + moves.emplace_back(from, to, std::nullopt, false, false, false); + } + } +} + +/** + * @brief Generates all double-square pawn pushes from the given square. + * + * Only allowed from the starting rank. Ensures the intermediate square is empty + * and respects occupancy, check mask, and pin mask constraints. + * + * @param moves Vector to append generated moves to + * @param from Source square of the pawn + * @param us Color of the pawn + * @param occupied Bitboard of all occupied squares + * @param check_mask Bitboard mask to restrict moves under check + * @param pin_mask Bitboard mask to restrict moves due to pins + */ +inline void generate_double_push(std::vector& moves, Square from, Color us, const Bitboard& occupied, + const Bitboard& check_mask, const Bitboard& pin_mask) { + const auto& single_push = Lookups::PAWN_SINGLE_PUSH[ColorUtil::to_index(us)]; + const auto& double_push = Lookups::PAWN_DOUBLE_PUSH[ColorUtil::to_index(us)]; + + if (!is_starting_rank(from, us)) { + return; + } + + Bitboard bb = double_push[from.flat_index()]; + bb &= ~occupied; + bb &= check_mask; + bb &= pin_mask; + + Bitboard single_bb = single_push[from.flat_index()] & occupied; + if (single_bb.empty() && bb) { + Square to = bb.pop_lsb().value(); + moves.emplace_back(from, to, std::nullopt, false, false, false); + } +} + +/** + * @brief Generates all pawn capture moves from the given square. + * + * Only captures squares occupied by enemy pieces. Applies check mask and pin mask. + * Automatically adds promotion moves if the destination is on the promotion rank. + * + * @param moves Vector to append generated moves to + * @param from Source square of the pawn + * @param us Color of the pawn + * @param enemy Bitboard of enemy pieces + * @param check_mask Bitboard mask to restrict moves under check + * @param pin_mask Bitboard mask to restrict moves due to pins + */ +inline void generate_captures(std::vector& moves, Square from, Color us, const Bitboard& enemy, + const Bitboard& check_mask, const Bitboard& pin_mask) { + const auto& captures = Lookups::PAWN_ATTACKS[ColorUtil::to_index(us)]; + + Bitboard bb = captures[from.flat_index()]; + bb &= enemy; + bb &= check_mask; + bb &= pin_mask; + + for (Square to : bb) { + if (is_promotion_rank(to, us)) { + add_pawn_promotions(moves, from, to, us, true); + } else { + moves.emplace_back(from, to, std::nullopt, true, false, false); + } + } +} + +/** + * @brief Generates a pawn en passant move from the given square, if legal. + * + * Verifies en passant square exists, the pawn can capture, and that + * performing the move does not leave the king in check. + * + * @param moves Vector to append generated moves to + * @param from Source square of the pawn + * @param us Color of the pawn + * @param board Current board position + * @param king_sq Square of the king for this side + * @param check_mask Bitboard mask to restrict moves under check + * @param pin_mask Bitboard mask to restrict moves due to pins + */ +inline void generate_en_passant(std::vector& moves, Square from, Color us, const Board& board, Square king_sq, + const Bitboard& check_mask, const Bitboard& pin_mask) { + const std::optional epsq_opt = board.en_passant_square(); + + if (!epsq_opt || !can_capture_en_passant(from, epsq_opt.value(), us)) { + return; + } + + const Color them = ColorUtil::opposite(us); + Square epsq = epsq_opt.value(); + auto bb = Bitboard(epsq); + bb &= check_mask; + bb &= pin_mask; + + if (!bb) { + return; + } + + Square cap_sq = (us == Color::WHITE) ? Square(epsq.flat_index() - Const::BOARD_WIDTH) + : Square(epsq.flat_index() + Const::BOARD_WIDTH); + + Board tmp(board); + tmp.remove_piece(cap_sq); + tmp.move_piece(from, epsq); + + Bitboard attackers = generate_attacks(tmp, them); + + if (!attackers.test(king_sq)) { + moves.emplace_back(from, epsq, std::nullopt, true, true, false); + } +} + +/** + * @brief Generates all legal pawn moves for the given side. + * + * Includes single pushes, double pushes, captures, promotions, and en passant, + * taking into account occupancy, checks, and pins. Iterates over all pawns of + * the specified color. + * + * @param moves Vector to append generated moves to + * @param board Current board position + * @param us Color of the side to generate moves for + * @param king_sq Square of the king for this side + * @param check_mask Bitboard mask to restrict moves under check + * @param pins Pin result structure indicating which pieces are pinned + */ +void generate_pawn_legal_moves(std::vector& moves, const Board& board, Color us, Square king_sq, + const Bitboard& check_mask, const PinResult& pins) { + const Bitboard enemy = board.enemy(us); + const Bitboard occupied = board.occupied(); + Bitboard pawns = board.pawns(us); + + while (auto from_sq = pawns.pop_lsb()) { + Square from = from_sq.value(); + const bool is_pinned = pins.pinned.test(from); + const Bitboard pin_mask = is_pinned ? pins.pin_ray[from.flat_index()] : Bitboard::Ones(); + + generate_single_push(moves, from, us, occupied, check_mask, pin_mask); + generate_double_push(moves, from, us, occupied, check_mask, pin_mask); + generate_captures(moves, from, us, enemy, check_mask, pin_mask); + generate_en_passant(moves, from, us, board, king_sq, check_mask, pin_mask); + } +} diff --git a/include/bitbishop/square.hpp b/include/bitbishop/square.hpp index 11fcbe3..b9e624e 100644 --- a/include/bitbishop/square.hpp +++ b/include/bitbishop/square.hpp @@ -134,6 +134,12 @@ class Square { */ [[nodiscard]] constexpr Value value() const { return m_value; } + /** + * @brief Get the underlying flqttened square index. + * @return std::uint8_t Flattened index corresponding to this square. + */ + [[nodiscard]] constexpr std::uint8_t flat_index() const { return static_cast(m_value); } + /** * @brief Get the file index (column). * @return Integer 0–7, where 0 = 'a' and 7 = 'h'. diff --git a/src/bitbishop/board.cpp b/src/bitbishop/board.cpp index 923ffa1..0976b52 100644 --- a/src/bitbishop/board.cpp +++ b/src/bitbishop/board.cpp @@ -129,6 +129,25 @@ std::optional Board::get_piece(Square square) const { return std::nullopt; // NO_PIECE } +void Board::move_piece(Square from, Square to) { + if (from == to) { + return; + } + + if (auto captured = get_piece(to)) { + remove_piece(to); + } + + auto moving_piece = get_piece(from); + if (!moving_piece) { + return; + } + + remove_piece(from); + + set_piece(to, moving_piece.value()); +} + void Board::set_piece(Square square, Piece piece) { // Remove any existing piece if existent const std::optional existing_piece = get_piece(square); @@ -249,3 +268,23 @@ bool Board::can_castle_queenside(Color side) const noexcept { const Bitboard occupied = this->occupied(); return !occupied.test(b_sq) && !occupied.test(c_sq) && !occupied.test(d_sq); } + +bool Board::operator==(const Board& other) const { + if (this == &other) { + return true; + } + + // Do not compare half-move clock and full-move number + // This is not relevant for position identity and we don't care about game history equality + return m_w_pawns == other.m_w_pawns && m_w_rooks == other.m_w_rooks && m_w_bishops == other.m_w_bishops && + m_w_knights == other.m_w_knights && m_w_king == other.m_w_king && m_w_queens == other.m_w_queens && + m_b_pawns == other.m_b_pawns && m_b_rooks == other.m_b_rooks && m_b_bishops == other.m_b_bishops && + m_b_knights == other.m_b_knights && m_b_king == other.m_b_king && m_b_queens == other.m_b_queens && + m_is_white_turn == other.m_is_white_turn && m_en_passant_sq == other.m_en_passant_sq && + m_white_castle_kingside == other.m_white_castle_kingside && + m_white_castle_queenside == other.m_white_castle_queenside && + m_black_castle_kingside == other.m_black_castle_kingside && + m_black_castle_queenside == other.m_black_castle_queenside; +} + +bool Board::operator!=(const Board& other) const { return !this->operator==(other); } diff --git a/tests/bitbishop/board/test_b_constructors.cpp b/tests/bitbishop/board/test_b_constructors.cpp index 2ad50cb..112f1c0 100644 --- a/tests/bitbishop/board/test_b_constructors.cpp +++ b/tests/bitbishop/board/test_b_constructors.cpp @@ -4,6 +4,9 @@ #include #include +using namespace Squares; +using namespace Pieces; + /** * @test BoardTest.DefaultStartingPosDefaultConstructor * @brief Verifies that a newly constructed board has the default FEN starting postion @@ -12,15 +15,15 @@ TEST(BoardTest, DefaultStartingPosDefaultConstructor) { Board board; // Check major pieces - EXPECT_EQ(board.get_piece(Square(0, 0)), Pieces::WHITE_ROOK); - EXPECT_EQ(board.get_piece(Square(4, 0)), Pieces::WHITE_KING); - EXPECT_EQ(board.get_piece(Square(0, 7)), Pieces::BLACK_ROOK); - EXPECT_EQ(board.get_piece(Square(4, 7)), Pieces::BLACK_KING); + EXPECT_EQ(board.get_piece(Square(0, 0)), WHITE_ROOK); + EXPECT_EQ(board.get_piece(Square(4, 0)), WHITE_KING); + EXPECT_EQ(board.get_piece(Square(0, 7)), BLACK_ROOK); + EXPECT_EQ(board.get_piece(Square(4, 7)), BLACK_KING); // Pawns for (int file = 0; file < 8; ++file) { - EXPECT_EQ(board.get_piece(Square(file, 1)), Pieces::WHITE_PAWN); - EXPECT_EQ(board.get_piece(Square(file, 6)), Pieces::BLACK_PAWN); + EXPECT_EQ(board.get_piece(Square(file, 1)), WHITE_PAWN); + EXPECT_EQ(board.get_piece(Square(file, 6)), BLACK_PAWN); } EXPECT_EQ(board.white_pieces().value() & board.black_pieces().value(), 0ULL); // no overlap @@ -38,17 +41,94 @@ TEST(BoardTest, FENConstructor) { Board board(start_fen); // Check major pieces - EXPECT_EQ(board.get_piece(Square(0, 0)), Pieces::WHITE_ROOK); - EXPECT_EQ(board.get_piece(Square(4, 0)), Pieces::WHITE_KING); - EXPECT_EQ(board.get_piece(Square(0, 7)), Pieces::BLACK_ROOK); - EXPECT_EQ(board.get_piece(Square(4, 7)), Pieces::BLACK_KING); + EXPECT_EQ(board.get_piece(Square(0, 0)), WHITE_ROOK); + EXPECT_EQ(board.get_piece(Square(4, 0)), WHITE_KING); + EXPECT_EQ(board.get_piece(Square(0, 7)), BLACK_ROOK); + EXPECT_EQ(board.get_piece(Square(4, 7)), BLACK_KING); // Pawns for (int file = 0; file < 8; ++file) { - EXPECT_EQ(board.get_piece(Square(file, 1)), Pieces::WHITE_PAWN); - EXPECT_EQ(board.get_piece(Square(file, 6)), Pieces::BLACK_PAWN); + EXPECT_EQ(board.get_piece(Square(file, 1)), WHITE_PAWN); + EXPECT_EQ(board.get_piece(Square(file, 6)), BLACK_PAWN); } EXPECT_EQ(board.white_pieces().value() & board.black_pieces().value(), 0ULL); // no overlap EXPECT_EQ(board.occupied().value(), board.white_pieces().value() | board.black_pieces().value()); } + +/** + * @test Copy constructor preserves board state. + * @brief Confirms that copying a board with the copy constructor produces an identical board. + */ +TEST(BoardCopyTest, CopyConstructor) { + Board original = Board::Empty(); + + // Set up pieces and state + original.set_piece(E1, WHITE_KING); + original.set_piece(E8, BLACK_KING); + original.set_piece(D4, WHITE_QUEEN); + original.set_piece(A7, BLACK_PAWN); + + // Set castling rights manually (assuming setters exist) + // original.set_white_castle_kingside(true); + // original.set_black_castle_queenside(true); + + Board copy(original); // Copy constructor + + // Piece positions + EXPECT_EQ(copy.get_piece(E1), WHITE_KING); + EXPECT_EQ(copy.get_piece(E8), BLACK_KING); + EXPECT_EQ(copy.get_piece(D4), WHITE_QUEEN); + EXPECT_EQ(copy.get_piece(A7), BLACK_PAWN); + + // Empty squares + EXPECT_EQ(copy.get_piece(E2), std::nullopt); + + // Ensure equality operator confirms identical boards + EXPECT_EQ(copy, original); +} + +/** + * @test Copy assignment preserves board state. + * @brief Confirms that copying a board via assignment produces an identical board. + */ +TEST(BoardCopyTest, CopyAssignment) { + Board original = Board::Empty(); + + // Set up pieces and state + original.set_piece(E1, WHITE_KING); + original.set_piece(E8, BLACK_KING); + original.set_piece(C3, WHITE_BISHOP); + original.set_piece(H7, BLACK_PAWN); + + Board copy = Board::Empty(); + copy = original; // Copy assignment + + // Piece positions + EXPECT_EQ(copy.get_piece(E1), WHITE_KING); + EXPECT_EQ(copy.get_piece(E8), BLACK_KING); + EXPECT_EQ(copy.get_piece(C3), WHITE_BISHOP); + EXPECT_EQ(copy.get_piece(H7), BLACK_PAWN); + + // Empty squares + EXPECT_EQ(copy.get_piece(D4), std::nullopt); + + // Ensure equality operator confirms identical boards + EXPECT_EQ(copy, original); +} + +/** + * @test Copy produces independent boards. + * @brief Modifying the copy does not affect the original board. + */ +TEST(BoardCopyTest, IndependenceAfterCopy) { + Board original = Board::Empty(); + original.set_piece(E1, WHITE_KING); + + Board copy(original); + copy.set_piece(E2, WHITE_PAWN); + + // Original should remain unchanged + EXPECT_EQ(original.get_piece(E2), std::nullopt); + EXPECT_EQ(copy.get_piece(E2), WHITE_PAWN); +} diff --git a/tests/bitbishop/board/test_b_equality_operators.cpp b/tests/bitbishop/board/test_b_equality_operators.cpp new file mode 100644 index 0000000..704d6ba --- /dev/null +++ b/tests/bitbishop/board/test_b_equality_operators.cpp @@ -0,0 +1,270 @@ +#include + +#include +#include + +using namespace Squares; +using namespace Pieces; + +/** + * @test Empty boards are equal. + * @brief Confirms two empty boards compare as equal. + */ +TEST(BoardEqualityTest, EmptyBoardsEqual) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + EXPECT_TRUE(board1 == board2); + EXPECT_FALSE(board1 != board2); +} + +/** + * @test Starting positions are equal. + * @brief Confirms two starting position boards compare as equal. + */ +TEST(BoardEqualityTest, StartingPositionsEqual) { + Board board1 = Board::StartingPosition(); + Board board2 = Board::StartingPosition(); + + EXPECT_TRUE(board1 == board2); + EXPECT_FALSE(board1 != board2); +} + +/** + * @test Board equals itself. + * @brief Confirms a board compares equal to itself. + */ +TEST(BoardEqualityTest, BoardEqualsItself) { + Board board = Board::StartingPosition(); + + EXPECT_TRUE(board == board); + EXPECT_FALSE(board != board); +} + +/** + * @test Different piece placement makes boards unequal. + * @brief Confirms boards with different pieces are not equal. + */ +TEST(BoardEqualityTest, DifferentPiecePlacement) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + board1.set_piece(E4, WHITE_PAWN); + board2.set_piece(E5, WHITE_PAWN); + + EXPECT_FALSE(board1 == board2); + EXPECT_TRUE(board1 != board2); +} + +/** + * @test Different piece types makes boards unequal. + * @brief Confirms boards with different piece types on same square are not equal. + */ +TEST(BoardEqualityTest, DifferentPieceTypes) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + board1.set_piece(E4, WHITE_PAWN); + board2.set_piece(E4, WHITE_KNIGHT); + + EXPECT_FALSE(board1 == board2); + EXPECT_TRUE(board1 != board2); +} + +/** + * @test Different piece colors makes boards unequal. + * @brief Confirms boards with different colored pieces on same square are not equal. + */ +TEST(BoardEqualityTest, DifferentPieceColors) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + board1.set_piece(E4, WHITE_PAWN); + board2.set_piece(E4, BLACK_PAWN); + + EXPECT_FALSE(board1 == board2); + EXPECT_TRUE(board1 != board2); +} + +/** + * @test Extra piece makes boards unequal. + * @brief Confirms board with additional piece is not equal. + */ +TEST(BoardEqualityTest, ExtraPieceMakesUnequal) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + board1.set_piece(E4, WHITE_PAWN); + board1.set_piece(D4, WHITE_PAWN); + board2.set_piece(E4, WHITE_PAWN); + + EXPECT_FALSE(board1 == board2); + EXPECT_TRUE(board1 != board2); +} + +/** + * @test Missing piece makes boards unequal. + * @brief Confirms board with missing piece is not equal. + */ +TEST(BoardEqualityTest, MissingPieceMakesUnequal) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + board1.set_piece(E4, WHITE_PAWN); + board2.set_piece(E4, WHITE_PAWN); + board2.set_piece(D4, WHITE_PAWN); + + EXPECT_FALSE(board1 == board2); + EXPECT_TRUE(board1 != board2); +} + +/** + * @test Boards with same pieces are equal. + * @brief Confirms boards with identical piece placement are equal. + */ +TEST(BoardEqualityTest, SamePiecesEqual) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + board1.set_piece(E4, WHITE_PAWN); + board1.set_piece(E5, BLACK_PAWN); + board1.set_piece(D4, WHITE_KNIGHT); + + board2.set_piece(E4, WHITE_PAWN); + board2.set_piece(E5, BLACK_PAWN); + board2.set_piece(D4, WHITE_KNIGHT); + + EXPECT_TRUE(board1 == board2); + EXPECT_FALSE(board1 != board2); +} + +/** + * @test Complex position equality. + * @brief Confirms equality works for complex board positions. + */ +TEST(BoardEqualityTest, ComplexPositionEquality) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + // Set up complex position + board1.set_piece(E1, WHITE_KING); + board1.set_piece(A1, WHITE_ROOK); + board1.set_piece(H1, WHITE_ROOK); + board1.set_piece(D2, WHITE_PAWN); + board1.set_piece(E2, WHITE_PAWN); + board1.set_piece(F2, WHITE_PAWN); + + board1.set_piece(E8, BLACK_KING); + board1.set_piece(A8, BLACK_ROOK); + board1.set_piece(D7, BLACK_PAWN); + board1.set_piece(E7, BLACK_PAWN); + + board2.set_piece(E1, WHITE_KING); + board2.set_piece(A1, WHITE_ROOK); + board2.set_piece(H1, WHITE_ROOK); + board2.set_piece(D2, WHITE_PAWN); + board2.set_piece(E2, WHITE_PAWN); + board2.set_piece(F2, WHITE_PAWN); + + board2.set_piece(E8, BLACK_KING); + board2.set_piece(A8, BLACK_ROOK); + board2.set_piece(D7, BLACK_PAWN); + board2.set_piece(E7, BLACK_PAWN); + + EXPECT_TRUE(board1 == board2); + EXPECT_FALSE(board1 != board2); +} + +/** + * @test Equality is symmetric. + * @brief Confirms that if board1 == board2, then board2 == board1. + */ +TEST(BoardEqualityTest, EqualityIsSymmetric) { + Board board1 = Board::StartingPosition(); + Board board2 = Board::StartingPosition(); + + EXPECT_TRUE(board1 == board2); + EXPECT_TRUE(board2 == board1); +} + +/** + * @test Inequality is symmetric. + * @brief Confirms that if board1 != board2, then board2 != board1. + */ +TEST(BoardEqualityTest, InequalityIsSymmetric) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + board1.set_piece(E4, WHITE_PAWN); + + EXPECT_TRUE(board1 != board2); + EXPECT_TRUE(board2 != board1); +} + +/** + * @test Equality is transitive. + * @brief Confirms that if board1 == board2 and board2 == board3, then board1 == board3. + */ +TEST(BoardEqualityTest, EqualityIsTransitive) { + Board board1 = Board::StartingPosition(); + Board board2 = Board::StartingPosition(); + Board board3 = Board::StartingPosition(); + + EXPECT_TRUE(board1 == board2); + EXPECT_TRUE(board2 == board3); + EXPECT_TRUE(board1 == board3); +} + +/** + * @test Empty vs non-empty board. + * @brief Confirms empty board is not equal to board with pieces. + */ +TEST(BoardEqualityTest, EmptyVsNonEmpty) { + Board board1 = Board::Empty(); + Board board2 = Board::StartingPosition(); + + EXPECT_FALSE(board1 == board2); + EXPECT_TRUE(board1 != board2); +} + +/** + * @test Order of piece placement doesn't matter. + * @brief Confirms boards are equal regardless of order pieces were placed. + */ +TEST(BoardEqualityTest, OrderOfPlacementDoesNotMatter) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + // Place pieces in different order + board1.set_piece(E4, WHITE_PAWN); + board1.set_piece(D4, WHITE_KNIGHT); + board1.set_piece(F4, WHITE_BISHOP); + + board2.set_piece(F4, WHITE_BISHOP); + board2.set_piece(E4, WHITE_PAWN); + board2.set_piece(D4, WHITE_KNIGHT); + + EXPECT_TRUE(board1 == board2); + EXPECT_FALSE(board1 != board2); +} + +/** + * @test All squares checked for equality. + * @brief Confirms all 64 squares are considered in equality check. + */ +TEST(BoardEqualityTest, AllSquaresChecked) { + Board board1 = Board::Empty(); + Board board2 = Board::Empty(); + + // Place piece on corner square + board1.set_piece(A1, WHITE_PAWN); + board2.set_piece(A1, WHITE_PAWN); + + EXPECT_TRUE(board1 == board2); + + // Change piece on opposite corner + board2.set_piece(H8, BLACK_PAWN); + + EXPECT_FALSE(board1 == board2); + EXPECT_TRUE(board1 != board2); +} diff --git a/tests/bitbishop/board/test_b_king_square.cpp b/tests/bitbishop/board/test_b_king_square.cpp new file mode 100644 index 0000000..b754748 --- /dev/null +++ b/tests/bitbishop/board/test_b_king_square.cpp @@ -0,0 +1,85 @@ +#include + +#include +#include + +using namespace Squares; +using namespace Pieces; + +/** + * @test King square on starting position. + * @brief Confirms king_square() returns correct squares for a normal starting board. + */ +TEST(BoardKingSquareTest, StartingPosition) { + Board board = Board::StartingPosition(); + + auto white_king_sq = board.king_square(Color::WHITE); + auto black_king_sq = board.king_square(Color::BLACK); + + ASSERT_TRUE(white_king_sq.has_value()); + ASSERT_TRUE(black_king_sq.has_value()); + + EXPECT_EQ(white_king_sq.value(), E1); + EXPECT_EQ(black_king_sq.value(), E8); +} + +/** + * @test King square on empty board. + * @brief Confirms king_square() returns std::nullopt when no kings are present. + */ +TEST(BoardKingSquareTest, EmptyBoard) { + Board board = Board::Empty(); + + auto white_king_sq = board.king_square(Color::WHITE); + auto black_king_sq = board.king_square(Color::BLACK); + + EXPECT_FALSE(white_king_sq.has_value()); + EXPECT_FALSE(black_king_sq.has_value()); +} + +/** + * @test King square with only white king present. + * @brief Confirms king_square() returns std::nullopt for missing black king. + */ +TEST(BoardKingSquareTest, OnlyWhiteKing) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + + auto white_king_sq = board.king_square(Color::WHITE); + auto black_king_sq = board.king_square(Color::BLACK); + + ASSERT_TRUE(white_king_sq.has_value()); + EXPECT_EQ(white_king_sq.value(), E1); + EXPECT_FALSE(black_king_sq.has_value()); +} + +/** + * @test King square with only black king present. + * @brief Confirms king_square() returns std::nullopt for missing white king. + */ +TEST(BoardKingSquareTest, OnlyBlackKing) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + + auto white_king_sq = board.king_square(Color::WHITE); + auto black_king_sq = board.king_square(Color::BLACK); + + EXPECT_FALSE(white_king_sq.has_value()); + ASSERT_TRUE(black_king_sq.has_value()); + EXPECT_EQ(black_king_sq.value(), E8); +} + +/** + * @test King square after moving the king. + * @brief Confirms king_square() updates correctly when a king is moved. + */ +TEST(BoardKingSquareTest, KingMoved) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + + board.move_piece(E1, G1); // Simulate castling move + + auto white_king_sq = board.king_square(Color::WHITE); + EXPECT_TRUE(white_king_sq.has_value()); + EXPECT_EQ(white_king_sq.value(), G1); +} diff --git a/tests/bitbishop/board/test_b_move_piece.cpp b/tests/bitbishop/board/test_b_move_piece.cpp new file mode 100644 index 0000000..ae2cddf --- /dev/null +++ b/tests/bitbishop/board/test_b_move_piece.cpp @@ -0,0 +1,439 @@ +#include + +#include +#include + +using namespace Squares; +using namespace Pieces; + +/** + * @test Move piece to empty square. + * @brief Confirms move_piece() correctly moves a piece to an empty square. + */ +TEST(BoardMovePieceTest, MoveToEmptySquare) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + board.move_piece(E2, E4); + + EXPECT_EQ(board.get_piece(E2), std::nullopt); + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); +} + +/** + * @test Move piece captures enemy piece. + * @brief Confirms move_piece() removes captured piece and places moving piece. + */ +TEST(BoardMovePieceTest, CaptureEnemyPiece) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E4, BLACK_PAWN); + + board.move_piece(E2, E4); + + EXPECT_EQ(board.get_piece(E2), std::nullopt); + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); +} + +/** + * @test Move piece captures friendly piece. + * @brief Confirms move_piece() removes captured friendly piece (though illegal + * in chess, the function should handle it). + */ +TEST(BoardMovePieceTest, CaptureFriendlyPiece) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E4, WHITE_KNIGHT); + + board.move_piece(E2, E4); + + EXPECT_EQ(board.get_piece(E2), std::nullopt); + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); +} + +/** + * @test Move from empty square does nothing. + * @brief Confirms move_piece() silently returns when source square is empty. + */ +TEST(BoardMovePieceTest, MoveFromEmptySquare) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KNIGHT); + + board.move_piece(E2, E3); + + EXPECT_EQ(board.get_piece(E2), std::nullopt); + EXPECT_EQ(board.get_piece(E3), std::nullopt); + EXPECT_EQ(board.get_piece(E4), WHITE_KNIGHT); +} + +/** + * @test Move white pawn. + * @brief Confirms move_piece() correctly handles white pawn movement. + */ +TEST(BoardMovePieceTest, MoveWhitePawn) { + Board board = Board::Empty(); + board.set_piece(A2, WHITE_PAWN); + + board.move_piece(A2, A4); + + EXPECT_EQ(board.get_piece(A2), std::nullopt); + EXPECT_EQ(board.get_piece(A4), WHITE_PAWN); +} + +/** + * @test Move black pawn. + * @brief Confirms move_piece() correctly handles black pawn movement. + */ +TEST(BoardMovePieceTest, MoveBlackPawn) { + Board board = Board::Empty(); + board.set_piece(A7, BLACK_PAWN); + + board.move_piece(A7, A5); + + EXPECT_EQ(board.get_piece(A7), std::nullopt); + EXPECT_EQ(board.get_piece(A5), BLACK_PAWN); +} + +/** + * @test Move white knight. + * @brief Confirms move_piece() correctly handles white knight movement. + */ +TEST(BoardMovePieceTest, MoveWhiteKnight) { + Board board = Board::Empty(); + board.set_piece(B1, WHITE_KNIGHT); + + board.move_piece(B1, C3); + + EXPECT_EQ(board.get_piece(B1), std::nullopt); + EXPECT_EQ(board.get_piece(C3), WHITE_KNIGHT); +} + +/** + * @test Move black knight. + * @brief Confirms move_piece() correctly handles black knight movement. + */ +TEST(BoardMovePieceTest, MoveBlackKnight) { + Board board = Board::Empty(); + board.set_piece(B8, BLACK_KNIGHT); + + board.move_piece(B8, C6); + + EXPECT_EQ(board.get_piece(B8), std::nullopt); + EXPECT_EQ(board.get_piece(C6), BLACK_KNIGHT); +} + +/** + * @test Move white bishop. + * @brief Confirms move_piece() correctly handles white bishop movement. + */ +TEST(BoardMovePieceTest, MoveWhiteBishop) { + Board board = Board::Empty(); + board.set_piece(C1, WHITE_BISHOP); + + board.move_piece(C1, F4); + + EXPECT_EQ(board.get_piece(C1), std::nullopt); + EXPECT_EQ(board.get_piece(F4), WHITE_BISHOP); +} + +/** + * @test Move black bishop. + * @brief Confirms move_piece() correctly handles black bishop movement. + */ +TEST(BoardMovePieceTest, MoveBlackBishop) { + Board board = Board::Empty(); + board.set_piece(C8, BLACK_BISHOP); + + board.move_piece(C8, F5); + + EXPECT_EQ(board.get_piece(C8), std::nullopt); + EXPECT_EQ(board.get_piece(F5), BLACK_BISHOP); +} + +/** + * @test Move white rook. + * @brief Confirms move_piece() correctly handles white rook movement. + */ +TEST(BoardMovePieceTest, MoveWhiteRook) { + Board board = Board::Empty(); + board.set_piece(A1, WHITE_ROOK); + + board.move_piece(A1, A4); + + EXPECT_EQ(board.get_piece(A1), std::nullopt); + EXPECT_EQ(board.get_piece(A4), WHITE_ROOK); +} + +/** + * @test Move black rook. + * @brief Confirms move_piece() correctly handles black rook movement. + */ +TEST(BoardMovePieceTest, MoveBlackRook) { + Board board = Board::Empty(); + board.set_piece(A8, BLACK_ROOK); + + board.move_piece(A8, A5); + + EXPECT_EQ(board.get_piece(A8), std::nullopt); + EXPECT_EQ(board.get_piece(A5), BLACK_ROOK); +} + +/** + * @test Move white queen. + * @brief Confirms move_piece() correctly handles white queen movement. + */ +TEST(BoardMovePieceTest, MoveWhiteQueen) { + Board board = Board::Empty(); + board.set_piece(D1, WHITE_QUEEN); + + board.move_piece(D1, D5); + + EXPECT_EQ(board.get_piece(D1), std::nullopt); + EXPECT_EQ(board.get_piece(D5), WHITE_QUEEN); +} + +/** + * @test Move black queen. + * @brief Confirms move_piece() correctly handles black queen movement. + */ +TEST(BoardMovePieceTest, MoveBlackQueen) { + Board board = Board::Empty(); + board.set_piece(D8, BLACK_QUEEN); + + board.move_piece(D8, D4); + + EXPECT_EQ(board.get_piece(D8), std::nullopt); + EXPECT_EQ(board.get_piece(D4), BLACK_QUEEN); +} + +/** + * @test Move white king. + * @brief Confirms move_piece() correctly handles white king movement. + */ +TEST(BoardMovePieceTest, MoveWhiteKing) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + + board.move_piece(E1, E2); + + EXPECT_EQ(board.get_piece(E1), std::nullopt); + EXPECT_EQ(board.get_piece(E2), WHITE_KING); +} + +/** + * @test Move black king. + * @brief Confirms move_piece() correctly handles black king movement. + */ +TEST(BoardMovePieceTest, MoveBlackKing) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + + board.move_piece(E8, E7); + + EXPECT_EQ(board.get_piece(E8), std::nullopt); + EXPECT_EQ(board.get_piece(E7), BLACK_KING); +} + +/** + * @test Move piece to same square. + * @brief Confirms move_piece() handles moving a piece to its own square + * (effectively a no-op, piece should remain). + */ +TEST(BoardMovePieceTest, MoveToSameSquare) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_KNIGHT); + + board.move_piece(E4, E4); + + EXPECT_EQ(board.get_piece(E4), WHITE_KNIGHT); +} + +/** + * @test Move across board edges. + * @brief Confirms move_piece() correctly handles moves from one edge to another. + */ +TEST(BoardMovePieceTest, MoveAcrossBoardEdges) { + Board board = Board::Empty(); + board.set_piece(A1, WHITE_ROOK); + + board.move_piece(A1, H8); + + EXPECT_EQ(board.get_piece(A1), std::nullopt); + EXPECT_EQ(board.get_piece(H8), WHITE_ROOK); +} + +/** + * @test Multiple sequential moves. + * @brief Confirms move_piece() correctly handles multiple moves in sequence. + */ +TEST(BoardMovePieceTest, MultipleSequentialMoves) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + board.move_piece(E2, E3); + board.move_piece(E3, E4); + board.move_piece(E4, E5); + + EXPECT_EQ(board.get_piece(E2), std::nullopt); + EXPECT_EQ(board.get_piece(E3), std::nullopt); + EXPECT_EQ(board.get_piece(E4), std::nullopt); + EXPECT_EQ(board.get_piece(E5), WHITE_PAWN); +} + +/** + * @test Move does not affect other pieces. + * @brief Confirms move_piece() only affects the from and to squares. + */ +TEST(BoardMovePieceTest, DoesNotAffectOtherPieces) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(D2, WHITE_PAWN); + board.set_piece(F2, WHITE_PAWN); + board.set_piece(E3, BLACK_PAWN); + + board.move_piece(E2, E4); + + EXPECT_EQ(board.get_piece(D2), WHITE_PAWN); + EXPECT_EQ(board.get_piece(F2), WHITE_PAWN); + EXPECT_EQ(board.get_piece(E3), BLACK_PAWN); + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); +} + +/** + * @test Bitboard consistency after move. + * @brief Confirms move_piece() maintains correct bitboard state. + */ +TEST(BoardMovePieceTest, BitboardConsistency) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E7, BLACK_PAWN); + + board.move_piece(E2, E4); + + Bitboard white_pawns = board.pawns(Color::WHITE); + Bitboard black_pawns = board.pawns(Color::BLACK); + + EXPECT_TRUE(white_pawns.test(E4)); + EXPECT_FALSE(white_pawns.test(E2)); + EXPECT_TRUE(black_pawns.test(E7)); + EXPECT_EQ(white_pawns.count(), 1); + EXPECT_EQ(black_pawns.count(), 1); +} + +/** + * @test Occupied squares after move. + * @brief Confirms move_piece() correctly updates occupied bitboard. + */ +TEST(BoardMovePieceTest, OccupiedSquaresAfterMove) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + board.move_piece(E2, E4); + + Bitboard occupied = board.occupied(); + + EXPECT_TRUE(occupied.test(E4)); + EXPECT_FALSE(occupied.test(E2)); + EXPECT_EQ(occupied.count(), 1); +} + +/** + * @test Occupied squares after capture. + * @brief Confirms move_piece() correctly updates occupied bitboard after capture. + */ +TEST(BoardMovePieceTest, OccupiedSquaresAfterCapture) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E4, BLACK_PAWN); + + Bitboard occupied_before = board.occupied(); + EXPECT_EQ(occupied_before.count(), 2); + + board.move_piece(E2, E4); + + Bitboard occupied_after = board.occupied(); + EXPECT_TRUE(occupied_after.test(E4)); + EXPECT_FALSE(occupied_after.test(E2)); + EXPECT_EQ(occupied_after.count(), 1); +} + +/** + * @test Move in starting position. + * @brief Confirms move_piece() works correctly from standard opening position. + */ +TEST(BoardMovePieceTest, MoveInStartingPosition) { + Board board = Board::StartingPosition(); + + board.move_piece(E2, E4); + + EXPECT_EQ(board.get_piece(E2), std::nullopt); + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); + + // Verify other pieces remain unchanged + EXPECT_EQ(board.get_piece(D2), WHITE_PAWN); + EXPECT_EQ(board.get_piece(F2), WHITE_PAWN); + EXPECT_EQ(board.get_piece(E1), WHITE_KING); +} + +/** + * @test Capture in starting position. + * @brief Confirms move_piece() handles captures from starting position. + */ +TEST(BoardMovePieceTest, CaptureInStartingPosition) { + Board board = Board::StartingPosition(); + + // Move white pawn forward twice + board.move_piece(E2, E4); + board.move_piece(E4, E5); + + // Capture black pawn on E7 (illegal move, but testing function behavior) + board.move_piece(E5, E7); + + EXPECT_EQ(board.get_piece(E7), WHITE_PAWN); + EXPECT_EQ(board.get_piece(E5), std::nullopt); +} + +/** + * @test Move updates piece type bitboards correctly. + * @brief Confirms move_piece() updates specific piece type bitboards. + */ +TEST(BoardMovePieceTest, UpdatesPieceTypeBitboards) { + Board board = Board::Empty(); + board.set_piece(B1, WHITE_KNIGHT); + + Bitboard knights_before = board.knights(Color::WHITE); + EXPECT_TRUE(knights_before.test(B1)); + EXPECT_FALSE(knights_before.test(C3)); + + board.move_piece(B1, C3); + + Bitboard knights_after = board.knights(Color::WHITE); + EXPECT_FALSE(knights_after.test(B1)); + EXPECT_TRUE(knights_after.test(C3)); + EXPECT_EQ(knights_after.count(), 1); +} + +/** + * @test Capture updates both color bitboards. + * @brief Confirms move_piece() correctly updates bitboards for both colors + * during a capture. + */ +TEST(BoardMovePieceTest, CaptureUpdatesBothColors) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_ROOK); + board.set_piece(E8, BLACK_ROOK); + + Bitboard white_rooks_before = board.rooks(Color::WHITE); + Bitboard black_rooks_before = board.rooks(Color::BLACK); + EXPECT_EQ(white_rooks_before.count(), 1); + EXPECT_EQ(black_rooks_before.count(), 1); + + board.move_piece(E4, E8); + + Bitboard white_rooks_after = board.rooks(Color::WHITE); + Bitboard black_rooks_after = board.rooks(Color::BLACK); + EXPECT_TRUE(white_rooks_after.test(E8)); + EXPECT_FALSE(white_rooks_after.test(E4)); + EXPECT_EQ(white_rooks_after.count(), 1); + EXPECT_EQ(black_rooks_after.count(), 0); +} diff --git a/tests/bitbishop/moves/pawn_move_generator/test_pmg_add_pawn_promotions.cpp b/tests/bitbishop/movegen/test_pawn_moves/test_add_pawn_promotions.cpp similarity index 77% rename from tests/bitbishop/moves/pawn_move_generator/test_pmg_add_pawn_promotions.cpp rename to tests/bitbishop/movegen/test_pawn_moves/test_add_pawn_promotions.cpp index 5880007..f7372de 100644 --- a/tests/bitbishop/moves/pawn_move_generator/test_pmg_add_pawn_promotions.cpp +++ b/tests/bitbishop/movegen/test_pawn_moves/test_add_pawn_promotions.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include /** * @brief Test fixture for pawn promotion move generation. @@ -24,7 +24,7 @@ TEST_F(PawnPromotionTest, WhitePromotionAdds4Moves) { Square from = Squares::E7; Square to = Squares::E8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); EXPECT_EQ(moves.size(), 4); } @@ -36,7 +36,7 @@ TEST_F(PawnPromotionTest, WhitePromotionContainsAllPieces) { Square from = Squares::E7; Square to = Squares::E8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); ASSERT_EQ(moves.size(), 4); @@ -53,7 +53,7 @@ TEST_F(PawnPromotionTest, WhitePromotionContainsSpecificMoves) { Square from = Squares::E7; Square to = Squares::E8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); Move queen_promo = {from, to, Pieces::WHITE_QUEEN, false, false, false}; Move rook_promo = {from, to, Pieces::WHITE_ROOK, false, false, false}; @@ -73,7 +73,7 @@ TEST_F(PawnPromotionTest, WhitePromotionHasCorrectSquares) { Square from = Squares::A7; Square to = Squares::A8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); ASSERT_EQ(moves.size(), 4); @@ -90,7 +90,7 @@ TEST_F(PawnPromotionTest, WhitePromotionNonCaptureFlags) { Square from = Squares::E7; Square to = Squares::E8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); ASSERT_EQ(moves.size(), 4); @@ -109,7 +109,7 @@ TEST_F(PawnPromotionTest, WhitePromotionCaptureFlags) { Square from = Squares::E7; Square to = Squares::F8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, true); + add_pawn_promotions(moves, from, to, Color::WHITE, true); ASSERT_EQ(moves.size(), 4); @@ -128,7 +128,7 @@ TEST_F(PawnPromotionTest, WhitePromotionCaptureContainsSpecificMoves) { Square from = Squares::E7; Square to = Squares::F8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, true); + add_pawn_promotions(moves, from, to, Color::WHITE, true); Move queen_promo = {from, to, Pieces::WHITE_QUEEN, true, false, false}; Move rook_promo = {from, to, Pieces::WHITE_ROOK, true, false, false}; @@ -148,7 +148,7 @@ TEST_F(PawnPromotionTest, BlackPromotionAdds4Moves) { Square from = Squares::A2; Square to = Squares::A1; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::BLACK, false); + add_pawn_promotions(moves, from, to, Color::BLACK, false); EXPECT_EQ(moves.size(), 4); } @@ -160,7 +160,7 @@ TEST_F(PawnPromotionTest, BlackPromotionContainsAllPieces) { Square from = Squares::A2; Square to = Squares::A1; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::BLACK, false); + add_pawn_promotions(moves, from, to, Color::BLACK, false); ASSERT_EQ(moves.size(), 4); @@ -177,7 +177,7 @@ TEST_F(PawnPromotionTest, BlackPromotionContainsSpecificMoves) { Square from = Squares::E2; Square to = Squares::E1; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::BLACK, false); + add_pawn_promotions(moves, from, to, Color::BLACK, false); Move queen_promo = {from, to, Pieces::BLACK_QUEEN, false, false, false}; Move rook_promo = {from, to, Pieces::BLACK_ROOK, false, false, false}; @@ -197,7 +197,7 @@ TEST_F(PawnPromotionTest, BlackPromotionHasCorrectSquares) { Square from = Squares::H2; Square to = Squares::H1; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::BLACK, false); + add_pawn_promotions(moves, from, to, Color::BLACK, false); ASSERT_EQ(moves.size(), 4); @@ -214,7 +214,7 @@ TEST_F(PawnPromotionTest, BlackPromotionNonCaptureFlags) { Square from = Squares::E2; Square to = Squares::E1; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::BLACK, false); + add_pawn_promotions(moves, from, to, Color::BLACK, false); ASSERT_EQ(moves.size(), 4); @@ -233,7 +233,7 @@ TEST_F(PawnPromotionTest, BlackPromotionCaptureFlags) { Square from = Squares::E2; Square to = Squares::D1; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::BLACK, true); + add_pawn_promotions(moves, from, to, Color::BLACK, true); ASSERT_EQ(moves.size(), 4); @@ -252,7 +252,7 @@ TEST_F(PawnPromotionTest, BlackPromotionCaptureContainsSpecificMoves) { Square from = Squares::E2; Square to = Squares::D1; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::BLACK, true); + add_pawn_promotions(moves, from, to, Color::BLACK, true); Move queen_promo = {from, to, Pieces::BLACK_QUEEN, true, false, false}; Move rook_promo = {from, to, Pieces::BLACK_ROOK, true, false, false}; @@ -274,7 +274,7 @@ TEST_F(PawnPromotionTest, PromotionAppendsToExistingMoves) { size_t initial_size = moves.size(); - PawnMoveGenerator::add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); + add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); EXPECT_EQ(moves.size(), initial_size + 4); } @@ -283,9 +283,9 @@ TEST_F(PawnPromotionTest, PromotionAppendsToExistingMoves) { * @test Verifies multiple promotion calls accumulate correctly (3 × 4 = 12 moves). */ TEST_F(PawnPromotionTest, MultiplePromotionCalls) { - PawnMoveGenerator::add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); - PawnMoveGenerator::add_pawn_promotions(moves, Squares::F7, Squares::G8, Color::WHITE, true); - PawnMoveGenerator::add_pawn_promotions(moves, Squares::E2, Squares::E1, Color::BLACK, false); + add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); + add_pawn_promotions(moves, Squares::F7, Squares::G8, Color::WHITE, true); + add_pawn_promotions(moves, Squares::E2, Squares::E1, Color::BLACK, false); EXPECT_EQ(moves.size(), 12); } @@ -295,22 +295,28 @@ TEST_F(PawnPromotionTest, MultiplePromotionCalls) { */ TEST_F(PawnPromotionTest, PromotionWithDifferentSquareCombinations) { std::vector> white_test_cases = { - {Squares::A7, Squares::A8}, {Squares::B7, Squares::B8}, {Squares::H7, Squares::H8}, {Squares::D7, Squares::E8} // capture + {Squares::A7, Squares::A8}, + {Squares::B7, Squares::B8}, + {Squares::H7, Squares::H8}, + {Squares::D7, Squares::E8} // capture }; for (const auto& [from, to] : white_test_cases) { std::vector temp_moves; - PawnMoveGenerator::add_pawn_promotions(temp_moves, from, to, Color::WHITE, false); + add_pawn_promotions(temp_moves, from, to, Color::WHITE, false); EXPECT_EQ(temp_moves.size(), 4); } std::vector> black_test_cases = { - {Squares::A2, Squares::A1}, {Squares::E2, Squares::E1}, {Squares::H2, Squares::H1}, {Squares::F2, Squares::E1} // capture + {Squares::A2, Squares::A1}, + {Squares::E2, Squares::E1}, + {Squares::H2, Squares::H1}, + {Squares::F2, Squares::E1} // capture }; for (const auto& [from, to] : black_test_cases) { std::vector temp_moves; - PawnMoveGenerator::add_pawn_promotions(temp_moves, from, to, Color::BLACK, false); + add_pawn_promotions(temp_moves, from, to, Color::BLACK, false); EXPECT_EQ(temp_moves.size(), 4); } } @@ -322,7 +328,7 @@ TEST_F(PawnPromotionTest, AllPromotionMovesHaveSameFromSquare) { Square from = Squares::E7; Square to = Squares::E8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); for (const auto& move : moves) { EXPECT_EQ(move.from, from); @@ -336,7 +342,7 @@ TEST_F(PawnPromotionTest, AllPromotionMovesHaveSameToSquare) { Square from = Squares::E7; Square to = Squares::E8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); for (const auto& move : moves) { EXPECT_EQ(move.to, to); @@ -350,26 +356,26 @@ TEST_F(PawnPromotionTest, AllPromotionMovesHaveSameCaptureFlag) { Square from = Squares::E7; Square to = Squares::E8; - PawnMoveGenerator::add_pawn_promotions(moves, from, to, Color::WHITE, false); + add_pawn_promotions(moves, from, to, Color::WHITE, false); for (const auto& move : moves) { EXPECT_FALSE(move.is_capture); } - moves.clear(); + moves.clear(); - Square cap_from = Squares::E7; - Square cap_to = Squares::F8; - PawnMoveGenerator::add_pawn_promotions(moves, cap_from, cap_to, Color::WHITE, true); - for (const auto& move : moves) { - EXPECT_TRUE(move.is_capture); - } + Square cap_from = Squares::E7; + Square cap_to = Squares::F8; + add_pawn_promotions(moves, cap_from, cap_to, Color::WHITE, true); + for (const auto& move : moves) { + EXPECT_TRUE(move.is_capture); + } } /** * @test Verifies promotion moves never have en passant flag set. */ TEST_F(PawnPromotionTest, NoPromotionMovesAreEnPassant) { - PawnMoveGenerator::add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); + add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); for (const auto& move : moves) { EXPECT_FALSE(move.is_en_passant); @@ -380,7 +386,7 @@ TEST_F(PawnPromotionTest, NoPromotionMovesAreEnPassant) { * @test Verifies promotion moves never have castling flag set. */ TEST_F(PawnPromotionTest, NoPromotionMovesAreCastles) { - PawnMoveGenerator::add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); + add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); for (const auto& move : moves) { EXPECT_FALSE(move.is_castling); @@ -391,7 +397,7 @@ TEST_F(PawnPromotionTest, NoPromotionMovesAreCastles) { * @test Verifies all promotion moves have a promotion piece specified. */ TEST_F(PawnPromotionTest, AllPromotionMovesHavePromotionPiece) { - PawnMoveGenerator::add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); + add_pawn_promotions(moves, Squares::E7, Squares::E8, Color::WHITE, false); for (const auto& move : moves) { EXPECT_TRUE(move.promotion.has_value()) << "All promotion moves must have a promotion piece"; diff --git a/tests/bitbishop/moves/pawn_move_generator/test_pmg_can_capture_en_passant.cpp b/tests/bitbishop/movegen/test_pawn_moves/test_can_capture_en_passant.cpp similarity index 92% rename from tests/bitbishop/moves/pawn_move_generator/test_pmg_can_capture_en_passant.cpp rename to tests/bitbishop/movegen/test_pawn_moves/test_can_capture_en_passant.cpp index 7bb632c..0b2accd 100644 --- a/tests/bitbishop/moves/pawn_move_generator/test_pmg_can_capture_en_passant.cpp +++ b/tests/bitbishop/movegen/test_pawn_moves/test_can_capture_en_passant.cpp @@ -1,6 +1,6 @@ #include -#include +#include /** * @brief Test case structure for en passant validation. @@ -30,7 +30,7 @@ TEST(PawnMoveGeneratorTest, WhiteCanCaptureEnPassant) { }; for (const EnPassantCase& c : cases) { - ASSERT_EQ(PawnMoveGenerator::can_capture_en_passant(c.from, c.en_passant, c.color), c.expected) + ASSERT_EQ(can_capture_en_passant(c.from, c.en_passant, c.color), c.expected) << "from=" << c.from.to_string() << " en_passant=" << c.en_passant.to_string() << " color=WHITE"; } } @@ -53,7 +53,7 @@ TEST(PawnMoveGeneratorTest, BlackCanCaptureEnPassant) { }; for (const EnPassantCase& c : cases) { - ASSERT_EQ(PawnMoveGenerator::can_capture_en_passant(c.from, c.en_passant, c.color), c.expected) + ASSERT_EQ(can_capture_en_passant(c.from, c.en_passant, c.color), c.expected) << "from=" << c.from.to_string() << " en_passant=" << c.en_passant.to_string() << " color=BLACK"; } } @@ -95,7 +95,7 @@ TEST(PawnMoveGeneratorTest, BlackCannotCaptureEnPassant_InvalidCombinations) { }; for (const EnPassantCase& c : cases) { - ASSERT_EQ(PawnMoveGenerator::can_capture_en_passant(c.from, c.en_passant, c.color), c.expected) + ASSERT_EQ(can_capture_en_passant(c.from, c.en_passant, c.color), c.expected) << "from=" << c.from.to_string() << " en_passant=" << c.en_passant.to_string() << " color=BLACK"; } } diff --git a/tests/bitbishop/movegen/test_pawn_moves/test_generate_pawn_legal_moves.cpp b/tests/bitbishop/movegen/test_pawn_moves/test_generate_pawn_legal_moves.cpp new file mode 100644 index 0000000..a79cf94 --- /dev/null +++ b/tests/bitbishop/movegen/test_pawn_moves/test_generate_pawn_legal_moves.cpp @@ -0,0 +1,760 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +/** + * @test Single white pawn push. + * @brief Confirms generate_pawn_legal_moves() generates single forward move + * for white pawn. + */ +TEST(GeneratePawnLegalMovesTest, WhiteSinglePush) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 2); + EXPECT_TRUE(contains_move(moves, {E2, E3, std::nullopt, false, false, false})); +} + +/** + * @test Single black pawn push. + * @brief Confirms generate_pawn_legal_moves() generates single forward move + * for black pawn. + */ +TEST(GeneratePawnLegalMovesTest, BlackSinglePush) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + board.set_piece(E7, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::BLACK, E8, check_mask, pins); + + EXPECT_EQ(moves.size(), 2); + EXPECT_TRUE(contains_move(moves, {E7, E6, std::nullopt, false, false, false})); +} + +/** + * @test White pawn double push from starting rank. + * @brief Confirms generate_pawn_legal_moves() generates double push for + * white pawn on second rank. + */ +TEST(GeneratePawnLegalMovesTest, WhiteDoublePush) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 2); + EXPECT_TRUE(contains_move(moves, {E2, E3, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E4, std::nullopt, false, false, false})); +} + +/** + * @test Black pawn double push from starting rank. + * @brief Confirms generate_pawn_legal_moves() generates double push for + * black pawn on seventh rank. + */ +TEST(GeneratePawnLegalMovesTest, BlackDoublePush) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + board.set_piece(E7, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::BLACK, E8, check_mask, pins); + + EXPECT_EQ(moves.size(), 2); + EXPECT_TRUE(contains_move(moves, {E7, E6, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E7, E5, std::nullopt, false, false, false})); +} + +/** + * @test Pawn blocked by friendly piece. + * @brief Confirms generate_pawn_legal_moves() generates no moves when + * pawn is blocked by friendly piece. + */ +TEST(GeneratePawnLegalMovesTest, BlockedByFriendlyPiece) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E3, WHITE_KNIGHT); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Pawn blocked by enemy piece. + * @brief Confirms generate_pawn_legal_moves() generates no forward moves + * when pawn is blocked by enemy piece. + */ +TEST(GeneratePawnLegalMovesTest, BlockedByEnemyPiece) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E3, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Double push blocked by piece on intermediate square. + * @brief Confirms generate_pawn_legal_moves() does not generate double push + * when intermediate square is blocked. + */ +TEST(GeneratePawnLegalMovesTest, DoublePushBlockedIntermediate) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E3, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 0); + EXPECT_FALSE(contains_move(moves, {E2, E4, std::nullopt, false, false, false})); +} + +/** + * @test Double push blocked by piece on target square. + * @brief Confirms generate_pawn_legal_moves() does not generate double push + * when target square is blocked. + */ +TEST(GeneratePawnLegalMovesTest, DoublePushBlockedTarget) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E4, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 1); + EXPECT_TRUE(contains_move(moves, {E2, E3, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {E2, E4, std::nullopt, false, false, false})); +} + +/** + * @test Pawn captures enemy piece diagonally. + * @brief Confirms generate_pawn_legal_moves() generates diagonal captures. + */ +TEST(GeneratePawnLegalMovesTest, DiagonalCapture) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, WHITE_PAWN); + board.set_piece(D5, BLACK_PAWN); + board.set_piece(F5, BLACK_KNIGHT); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 3); + EXPECT_TRUE(contains_move(moves, {E4, E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E4, D5, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {E4, F5, std::nullopt, true, false, false})); +} + +/** + * @test Pawn does not capture friendly pieces. + * @brief Confirms generate_pawn_legal_moves() does not generate captures + * of friendly pieces. + */ +TEST(GeneratePawnLegalMovesTest, NoFriendlyCapture) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E4, WHITE_PAWN); + board.set_piece(D5, WHITE_PAWN); + board.set_piece(F5, WHITE_KNIGHT); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 2); + EXPECT_TRUE(contains_move(moves, {E4, E5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {D5, D6, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {E4, D5, std::nullopt, true, false, false})); + EXPECT_FALSE(contains_move(moves, {E4, F5, std::nullopt, true, false, false})); +} + +/** + * @test White pawn promotion on eighth rank. + * @brief Confirms generate_pawn_legal_moves() generates all four promotion + * moves for white pawn reaching eighth rank. + */ +TEST(GeneratePawnLegalMovesTest, WhitePawnPromotion) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E7, WHITE_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 4); + 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 Black pawn promotion on first rank. + * @brief Confirms generate_pawn_legal_moves() generates all four promotion + * moves for black pawn reaching first rank. + */ +TEST(GeneratePawnLegalMovesTest, BlackPawnPromotion) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + board.set_piece(E2, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::BLACK, E8, check_mask, pins); + + EXPECT_EQ(moves.size(), 4); + EXPECT_TRUE(contains_move(moves, {E2, E1, BLACK_QUEEN, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E1, BLACK_ROOK, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E1, BLACK_BISHOP, false, false, false})); + EXPECT_TRUE(contains_move(moves, {E2, E1, BLACK_KNIGHT, false, false, false})); +} + +/** + * @test Capture with promotion. + * @brief Confirms generate_pawn_legal_moves() generates promotion captures. + */ +TEST(GeneratePawnLegalMovesTest, CaptureWithPromotion) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E7, WHITE_PAWN); + board.set_piece(D8, BLACK_ROOK); + board.set_piece(F8, BLACK_KNIGHT); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // 4 promotions forward + 4 capture promotions left + 4 capture promotions right = 12 + EXPECT_EQ(moves.size(), 12); + + // Check some capture promotions + EXPECT_TRUE(contains_move(moves, {E7, D8, WHITE_QUEEN, true, false, false})); + EXPECT_TRUE(contains_move(moves, {E7, F8, WHITE_QUEEN, true, false, false})); +} + +/** + * @test Check mask restricts pawn moves. + * @brief Confirms generate_pawn_legal_moves() only generates moves within + * check mask. + */ +TEST(GeneratePawnLegalMovesTest, CheckMaskRestriction) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + + Bitboard check_mask = Bitboard::Zeros(); + check_mask.set(E3); + + std::vector moves; + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 1); + EXPECT_TRUE(contains_move(moves, {E2, E3, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {E2, E4, std::nullopt, false, false, false})); +} + +/** + * @test Empty check mask generates no moves. + * @brief Confirms generate_pawn_legal_moves() generates no moves when + * check mask is empty. + */ +TEST(GeneratePawnLegalMovesTest, EmptyCheckMaskNoMoves) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Zeros(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Pinned pawn moves along pin ray only. + * @brief Confirms generate_pawn_legal_moves() restricts pinned pawn to + * moves along pin ray. + */ +TEST(GeneratePawnLegalMovesTest, PinnedPawnAlongRay) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E3, WHITE_PAWN); + board.set_piece(E8, BLACK_ROOK); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // Pawn can move forward along pin ray + EXPECT_EQ(moves.size(), 1); + EXPECT_TRUE(contains_move(moves, {E3, E4, std::nullopt, false, false, false})); +} + +/** + * @test Diagonally pinned pawn cannot move. + * @brief Confirms generate_pawn_legal_moves() generates no moves when + * pawn is pinned diagonally. + */ +TEST(GeneratePawnLegalMovesTest, DiagonallyPinnedPawnNoMoves) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(F2, WHITE_PAWN); + board.set_piece(H4, BLACK_BISHOP); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // Pawn pinned diagonally cannot move forward + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Diagonally pinned pawn can capture attacker. + * @brief Confirms generate_pawn_legal_moves() allows pinned pawn to + * capture along pin ray. + */ +TEST(GeneratePawnLegalMovesTest, DiagonallyPinnedPawnCapturesAttacker) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(F2, WHITE_PAWN); + board.set_piece(G3, BLACK_BISHOP); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 1); + EXPECT_TRUE(contains_move(moves, {F2, G3, std::nullopt, true, false, false})); +} + +/** + * @test Multiple pawns generate moves independently. + * @brief Confirms generate_pawn_legal_moves() generates moves for all pawns. + */ +TEST(GeneratePawnLegalMovesTest, MultiplePawns) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(D2, WHITE_PAWN); + board.set_piece(F2, WHITE_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // Each pawn has 2 moves (single and double push) = 6 total + EXPECT_EQ(moves.size(), 6); +} + +/** + * @test Pawn on edge of board. + * @brief Confirms generate_pawn_legal_moves() correctly handles pawns on + * board edges (only one capture available). + */ +TEST(GeneratePawnLegalMovesTest, PawnOnEdge) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(A4, WHITE_PAWN); + board.set_piece(B5, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 2); + EXPECT_TRUE(contains_move(moves, {A4, A5, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {A4, B5, std::nullopt, true, false, false})); +} + +/** + * @test No pawns on board. + * @brief Confirms generate_pawn_legal_moves() handles empty pawn bitboard. + */ +TEST(GeneratePawnLegalMovesTest, NoPawnsOnBoard) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Moves vector accumulates correctly. + * @brief Confirms generate_pawn_legal_moves() appends to existing moves. + */ +TEST(GeneratePawnLegalMovesTest, MovesVectorAccumulates) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + + std::vector moves; + moves.emplace_back(A1, A2, std::nullopt, false, false, false); + + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 3); + EXPECT_TRUE(contains_move(moves, {A1, A2, std::nullopt, false, false, false})); +} + +/** + * @test Starting position pawn moves. + * @brief Confirms generate_pawn_legal_moves() generates correct moves from + * starting position. + */ +TEST(GeneratePawnLegalMovesTest, StartingPositionPawns) { + Board board = Board::StartingPosition(); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // 8 pawns, each with 2 moves (single and double push) = 16 total + EXPECT_EQ(moves.size(), 16); +} + +/** + * @test Move properties are correct. + * @brief Confirms generate_pawn_legal_moves() sets all move properties + * correctly. + */ +TEST(GeneratePawnLegalMovesTest, MovePropertiesCorrect) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(D3, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + for (const Move& move : moves) { + EXPECT_EQ(move.from, E2); + EXPECT_FALSE(move.is_castling); + EXPECT_FALSE(move.is_en_passant); + + if (move.to == D3) { + EXPECT_TRUE(move.is_capture); + } else { + EXPECT_FALSE(move.is_capture); + } + } +} + +/** + * @test Pawn cannot move off starting rank without double push option. + * @brief Confirms pawn moved off starting rank loses double push ability. + */ +TEST(GeneratePawnLegalMovesTest, NoDoublePushOffStartingRank) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E3, WHITE_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + EXPECT_EQ(moves.size(), 1); + EXPECT_TRUE(contains_move(moves, {E3, E4, std::nullopt, false, false, false})); + EXPECT_FALSE(contains_move(moves, {E3, E5, std::nullopt, false, false, false})); +} + +/** + * @test White en passant capture. + * @brief Confirms generate_pawn_legal_moves() generates en passant capture + * for white pawn. + */ +TEST(GeneratePawnLegalMovesTest, WhiteEnPassantCapture) { + Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // Should include en passant capture + EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); +} + +/** + * @test Black en passant capture. + * @brief Confirms generate_pawn_legal_moves() generates en passant capture + * for black pawn. + */ +TEST(GeneratePawnLegalMovesTest, BlackEnPassantCapture) { + Board board("rnbqkbnr/ppp1pppp/8/8/3pP3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E8, board, Color::BLACK); + + generate_pawn_legal_moves(moves, board, Color::BLACK, E8, check_mask, pins); + + // Should include en passant capture + EXPECT_TRUE(contains_move(moves, {D4, E3, std::nullopt, true, true, false})); +} + +/** + * @test En passant on both sides. + * @brief Confirms generate_pawn_legal_moves() generates en passant when + * two pawns can capture same target. + */ +TEST(GeneratePawnLegalMovesTest, EnPassantBothSides) { + Board board("rnbqkbnr/ppp1pppp/8/2PpP3/8/8/PP1P1PPP/RNBQKBNR w KQkq d6 0 1"); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // Both pawns can capture en passant + EXPECT_TRUE(contains_move(moves, {C5, D6, std::nullopt, true, true, false})); + EXPECT_TRUE(contains_move(moves, {E5, D6, std::nullopt, true, true, false})); +} + +/** + * @test No en passant without target square. + * @brief Confirms generate_pawn_legal_moves() does not generate en passant + * when no en passant square is set. + */ +TEST(GeneratePawnLegalMovesTest, NoEnPassantWithoutTarget) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(D5, WHITE_PAWN); + board.set_piece(E5, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // No en passant without target square + EXPECT_FALSE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); +} + +/** + * @test En passant blocked by check mask. + * @brief Confirms generate_pawn_legal_moves() does not generate en passant + * when target square not in check mask. + */ +TEST(GeneratePawnLegalMovesTest, EnPassantBlockedByCheckMask) { + Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); + + Bitboard check_mask = Bitboard::Zeros(); + check_mask.set(D6); // En passant square E6 not in mask + + std::vector moves; + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // En passant not allowed + EXPECT_FALSE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); +} + +/** + * @test En passant blocked by pin. + * @brief Confirms generate_pawn_legal_moves() does not generate en passant + * when pawn is pinned perpendicular to en passant direction. + */ +TEST(GeneratePawnLegalMovesTest, EnPassantBlockedByPin) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(E5, WHITE_PAWN); + board.set_piece(D5, BLACK_PAWN); + board.set_piece(E8, BLACK_ROOK); + + // Manually set en passant square + Board board_with_ep("8/8/8/3pP3/8/8/8/4K2r b - e6 0 1"); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board_with_ep, Color::WHITE); + + generate_pawn_legal_moves(moves, board_with_ep, Color::WHITE, E1, check_mask, pins); + + // Pinned pawn cannot capture en passant + EXPECT_FALSE(contains_move(moves, {E5, D6, std::nullopt, true, true, false})); +} + +/** + * @test En passant reveals check (horizontal pin). + * @brief Confirms generate_pawn_legal_moves() does not generate en passant + * when capturing would expose king to horizontal attack. + */ +TEST(GeneratePawnLegalMovesTest, EnPassantRevealsHorizontalCheck) { + // White king on E5, white pawn on D5, black pawn on E5, black rook on A5 + // If white captures en passant on E6, removes both pawns and exposes king + Board board("8/8/8/r2PpK2/8/8/8/8 w - e6 0 1"); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(F5, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, F5, check_mask, pins); + + // En passant not allowed as it would expose king + EXPECT_FALSE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); +} + +/** + * @test En passant legal when safe. + * @brief Confirms generate_pawn_legal_moves() generates en passant when + * it doesn't expose king to check. + */ +TEST(GeneratePawnLegalMovesTest, EnPassantLegalWhenSafe) { + Board board("rnbqkbnr/ppp2ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // En passant is safe + EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); +} + +/** + * @test En passant with promotion (edge case). + * @brief Confirms en passant is only available on correct ranks + * (5th for white, 4th for black). + */ +TEST(GeneratePawnLegalMovesTest, EnPassantOnlyOnCorrectRank) { + // White pawn on 6th rank - cannot do en passant from here + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(D6, WHITE_PAWN); + board.set_piece(E6, BLACK_PAWN); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins; + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // No en passant from 6th rank + EXPECT_FALSE(contains_move(moves, {D6, E7, std::nullopt, true, true, false})); +} + +/** + * @test En passant move properties correct. + * @brief Confirms en passant move has correct flags set. + */ +TEST(GeneratePawnLegalMovesTest, EnPassantMoveProperties) { + Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); + + std::vector moves; + Bitboard check_mask = Bitboard::Ones(); + PinResult pins = compute_pins(E1, board, Color::WHITE); + + generate_pawn_legal_moves(moves, board, Color::WHITE, E1, check_mask, pins); + + // Find en passant move + bool found = false; + for (const Move& move : moves) { + if (move.from == D5 && move.to == E6) { + EXPECT_TRUE(move.is_capture); + EXPECT_TRUE(move.is_en_passant); + EXPECT_FALSE(move.is_castling); + EXPECT_FALSE(move.promotion.has_value()); + found = true; + break; + } + } + EXPECT_TRUE(found); +} diff --git a/tests/bitbishop/moves/pawn_move_generator/test_pmg_is_promotion_rank.cpp b/tests/bitbishop/movegen/test_pawn_moves/test_is_promotion_rank.cpp similarity index 61% rename from tests/bitbishop/moves/pawn_move_generator/test_pmg_is_promotion_rank.cpp rename to tests/bitbishop/movegen/test_pawn_moves/test_is_promotion_rank.cpp index de108c3..e783e4d 100644 --- a/tests/bitbishop/moves/pawn_move_generator/test_pmg_is_promotion_rank.cpp +++ b/tests/bitbishop/movegen/test_pawn_moves/test_is_promotion_rank.cpp @@ -1,14 +1,13 @@ #include -#include +#include /** * @test Verifies is_promotion_rank returns true for White pawns on rank 8 (A8-H8). */ TEST(PawnMoveGeneratorTest, IsPromotionRankFromPromotionRankForWhite) { for (int sq = Square::A8; sq <= Square::H8; sq++) { - ASSERT_TRUE(PawnMoveGenerator::is_promotion_rank(Square(sq), Color::WHITE)) - << "for square " << Square(sq).to_string(); + ASSERT_TRUE(is_promotion_rank(Square(sq), Color::WHITE)) << "for square " << Square(sq).to_string(); } } @@ -17,8 +16,7 @@ TEST(PawnMoveGeneratorTest, IsPromotionRankFromPromotionRankForWhite) { */ TEST(PawnMoveGeneratorTest, IsPromotionRankFromPromotionRankForBlack) { for (int sq = Square::A1; sq <= Square::H1; sq++) { - ASSERT_TRUE(PawnMoveGenerator::is_promotion_rank(Square(sq), Color::BLACK)) - << "for square " << Square(sq).to_string(); + ASSERT_TRUE(is_promotion_rank(Square(sq), Color::BLACK)) << "for square " << Square(sq).to_string(); } } @@ -27,8 +25,7 @@ TEST(PawnMoveGeneratorTest, IsPromotionRankFromPromotionRankForBlack) { */ TEST(PawnMoveGeneratorTest, IsPromotionRankNotFromPromotionRankForWhite) { for (int sq = Square::A1; sq <= Square::H7; sq++) { - ASSERT_FALSE(PawnMoveGenerator::is_promotion_rank(Square(sq), Color::WHITE)) - << "for square " << Square(sq).to_string(); + ASSERT_FALSE(is_promotion_rank(Square(sq), Color::WHITE)) << "for square " << Square(sq).to_string(); } } @@ -37,7 +34,6 @@ TEST(PawnMoveGeneratorTest, IsPromotionRankNotFromPromotionRankForWhite) { */ TEST(PawnMoveGeneratorTest, IsPromotionRankNotFromPromotionRankForBlack) { for (int sq = Square::A2; sq <= Square::H8; sq++) { - ASSERT_FALSE(PawnMoveGenerator::is_promotion_rank(Square(sq), Color::BLACK)) - << "for square " << Square(sq).to_string(); + ASSERT_FALSE(is_promotion_rank(Square(sq), Color::BLACK)) << "for square " << Square(sq).to_string(); } } diff --git a/tests/bitbishop/movegen/test_pawn_moves/test_is_starting_rank.cpp b/tests/bitbishop/movegen/test_pawn_moves/test_is_starting_rank.cpp new file mode 100644 index 0000000..4e0a7aa --- /dev/null +++ b/tests/bitbishop/movegen/test_pawn_moves/test_is_starting_rank.cpp @@ -0,0 +1,49 @@ +#include + +#include + +/** + * @test Verifies is_starting_rank returns true for White pawns on rank 2 (A2-H2). + */ +TEST(PawnMoveGeneratorTest, IsStartingRankFromStartingRankForWhite) { + for (int sq = Square::A2; sq <= Square::H2; sq++) { + ASSERT_TRUE(is_starting_rank(Square(sq), Color::WHITE)) << "for square " << Square(sq).to_string(); + } +} + +/** + * @test Verifies is_starting_rank returns true for Black pawns on rank 7 (A7-H7). + */ +TEST(PawnMoveGeneratorTest, IsStartingRankFromStartingRankForBlack) { + for (int sq = Square::A7; sq <= Square::H7; sq++) { + ASSERT_TRUE(is_starting_rank(Square(sq), Color::BLACK)) << "for square " << Square(sq).to_string(); + } +} + +/** + * @test Verifies is_starting_rank returns false for White pawns on all non-starting ranks. + */ +TEST(PawnMoveGeneratorTest, IsStartingRankFromNotSartingRankForWhite) { + // Rank 1 is not a starting rank + for (int sq = Square::A1; sq <= Square::H1; sq++) { + ASSERT_FALSE(is_starting_rank(Square(sq), Color::WHITE)) << "for square " << Square(sq).to_string(); + } + // Ranks 3-8 are not starting ranks + for (int sq = Square::A3; sq <= Square::H8; sq++) { + ASSERT_FALSE(is_starting_rank(Square(sq), Color::WHITE)) << "for square " << Square(sq).to_string(); + } +} + +/** + * @test Verifies is_starting_rank returns false for Black pawns on all non-starting ranks. + */ +TEST(PawnMoveGeneratorTest, IsStartingRankFromNotSartingRankForBlack) { + // Ranks 1-6 are not starting ranks + for (int sq = Square::A1; sq <= Square::H6; sq++) { + ASSERT_FALSE(is_starting_rank(Square(sq), Color::BLACK)) << "for square " << Square(sq).to_string(); + } + // Rank 8 is not a starting rank + for (int sq = Square::A8; sq <= Square::H8; sq++) { + ASSERT_FALSE(is_starting_rank(Square(sq), Color::BLACK)) << "for square " << Square(sq).to_string(); + } +} \ No newline at end of file diff --git a/tests/bitbishop/moves/pawn_move_generator/test_pmg_is_starting_rank.cpp b/tests/bitbishop/moves/pawn_move_generator/test_pmg_is_starting_rank.cpp deleted file mode 100644 index 75644fb..0000000 --- a/tests/bitbishop/moves/pawn_move_generator/test_pmg_is_starting_rank.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include - -#include - -/** - * @test Verifies is_starting_rank returns true for White pawns on rank 2 (A2-H2). - */ -TEST(PawnMoveGeneratorTest, IsStartingRankFromStartingRankForWhite) { - for (int sq = Square::A2; sq <= Square::H2; sq++) { - ASSERT_TRUE(PawnMoveGenerator::is_starting_rank(Square(sq), Color::WHITE)) - << "for square " << Square(sq).to_string(); - } -} - -/** - * @test Verifies is_starting_rank returns true for Black pawns on rank 7 (A7-H7). - */ -TEST(PawnMoveGeneratorTest, IsStartingRankFromStartingRankForBlack) { - for (int sq = Square::A7; sq <= Square::H7; sq++) { - ASSERT_TRUE(PawnMoveGenerator::is_starting_rank(Square(sq), Color::BLACK)) - << "for square " << Square(sq).to_string(); - } -} - -/** - * @test Verifies is_starting_rank returns false for White pawns on all non-starting ranks. - */ -TEST(PawnMoveGeneratorTest, IsStartingRankFromNotSartingRankForWhite) { - // Rank 1 is not a starting rank - for (int sq = Square::A1; sq <= Square::H1; sq++) { - ASSERT_FALSE(PawnMoveGenerator::is_starting_rank(Square(sq), Color::WHITE)) - << "for square " << Square(sq).to_string(); - } - // Ranks 3-8 are not starting ranks - for (int sq = Square::A3; sq <= Square::H8; sq++) { - ASSERT_FALSE(PawnMoveGenerator::is_starting_rank(Square(sq), Color::WHITE)) - << "for square " << Square(sq).to_string(); - } -} - -/** - * @test Verifies is_starting_rank returns false for Black pawns on all non-starting ranks. - */ -TEST(PawnMoveGeneratorTest, IsStartingRankFromNotSartingRankForBlack) { - // Ranks 1-6 are not starting ranks - for (int sq = Square::A1; sq <= Square::H6; sq++) { - ASSERT_FALSE(PawnMoveGenerator::is_starting_rank(Square(sq), Color::BLACK)) - << "for square " << Square(sq).to_string(); - } - // Rank 8 is not a starting rank - for (int sq = Square::A8; sq <= Square::H8; sq++) { - ASSERT_FALSE(PawnMoveGenerator::is_starting_rank(Square(sq), Color::BLACK)) - << "for square " << Square(sq).to_string(); - } -} \ No newline at end of file