From 0be1ca06732fec8d70695840872f7a881b2dce7c Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Sat, 13 Dec 2025 18:41:17 +0100 Subject: [PATCH 1/4] added rook per ray lookup --- include/bitbishop/lookups/rook.hpp | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/include/bitbishop/lookups/rook.hpp b/include/bitbishop/lookups/rook.hpp index 8e06c3b..314bc04 100644 --- a/include/bitbishop/lookups/rook.hpp +++ b/include/bitbishop/lookups/rook.hpp @@ -101,4 +101,60 @@ constexpr std::array ROOK_ATTACKS = []() constexpr return table; }(); +/** + * @brief Precomputed lookup table of rook north attacks for every square. + * Indexed by square (0-63). + */ +constexpr std::array ROOK_NORTH_ATTACKS = []() constexpr { + using namespace Const; + + std::array table{}; + for (int sq = 0; sq < BOARD_SIZE; ++sq) { + table[sq] = Bitboard(rook_north_attacks(sq)); + } + return table; +}(); + +/** + * @brief Precomputed lookup table of rook south attacks for every square. + * Indexed by square (0-63). + */ +constexpr std::array ROOK_SOUTH_ATTACKS = []() constexpr { + using namespace Const; + + std::array table{}; + for (int sq = 0; sq < BOARD_SIZE; ++sq) { + table[sq] = Bitboard(rook_south_attacks(sq)); + } + return table; +}(); + +/** + * @brief Precomputed lookup table of rook east attacks for every square. + * Indexed by square (0-63). + */ +constexpr std::array ROOK_EAST_ATTACKS = []() constexpr { + using namespace Const; + + std::array table{}; + for (int sq = 0; sq < BOARD_SIZE; ++sq) { + table[sq] = Bitboard(rook_east_attacks(sq)); + } + return table; +}(); + +/** + * @brief Precomputed lookup table of rook west attacks for every square. + * Indexed by square (0-63). + */ +constexpr std::array ROOK_WEST_ATTACKS = []() constexpr { + using namespace Const; + + std::array table{}; + for (int sq = 0; sq < BOARD_SIZE; ++sq) { + table[sq] = Bitboard(rook_west_attacks(sq)); + } + return table; +}(); + } // namespace Lookups From f71f4a80365bb6f7afe865761c5b9acdf42ab9c2 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Sat, 13 Dec 2025 19:31:26 +0100 Subject: [PATCH 2/4] implementation of rook moves with castling included --- include/bitbishop/moves/rook_move_gen.hpp | 80 +++++++++++++++ src/bitbishop/moves/rook_move_gen.cpp | 117 ++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 include/bitbishop/moves/rook_move_gen.hpp create mode 100644 src/bitbishop/moves/rook_move_gen.cpp diff --git a/include/bitbishop/moves/rook_move_gen.hpp b/include/bitbishop/moves/rook_move_gen.hpp new file mode 100644 index 0000000..a614304 --- /dev/null +++ b/include/bitbishop/moves/rook_move_gen.hpp @@ -0,0 +1,80 @@ +#pragma once +#include +#include +#include +#include + +/** + * @brief Generates rook moves. + * + * Rooks can move horizontally and vertically in the four directions (N, S, E, W) until a friendly + * or enemy piece is encountered. + * This namespace provides functions for generating both pseudo-legal and legal rook moves. + */ +namespace RookMoveGenerator { + +/** + * @brief Generates all pseudo-legal rook 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); + +/** + * @brief Adds castling moves to the move list if conditions are met. + * + * Validates castling legality and adds both kingside and queenside castling + * moves when available. Castling requires that the king and rook haven't moved, + * squares between them are empty, and the king doesn't move through check. + * + * @param moves Vector to append castling moves to + * @param from Square the king is currently on + * @param side Color of the side attempting to castle + * @param board Current board state + * + * @see https://www.chess.com/article/view/how-to-castle-in-chess + */ +void add_rook_castling(std::vector& moves, Square from, Color side, const Board& board); + +/** + * @brief Computes the north ray from a square, stopping at the first blocker. + * + * @param from The starting square + * @param occupied Bitboard of all occupied squares on the board + * @return Bitboard containing all squares occupied by friendly or enemy pieces + */ +Bitboard north_ray(Square from, const Bitboard& occupied); + +/** + * @brief Computes the south ray from a square, stopping at the first blocker. + * + * @param from The starting square + * @param occupied Bitboard of all occupied squares on the board + * @return Bitboard containing all squares occupied by friendly or enemy pieces + */ +Bitboard south_ray(Square from, const Bitboard& occupied); + +/** + * @brief Computes the east ray from a square, stopping at the first blocker. + * + * @param from The starting square + * @param occupied Bitboard of all occupied squares on the board + * @return Bitboard containing all squares occupied by friendly or enemy pieces + */ +Bitboard east_ray(Square from, const Bitboard& occupied); + +/** + * @brief Computes the west ray from a square, stopping at the first blocker. + * + * @param from The starting square + * @param occupied Bitboard of all occupied squares on the board + * @return Bitboard containing all squares occupied by friendly or enemy pieces + */ +Bitboard west_ray(Square from, const Bitboard& occupied); + +}; // namespace RookMoveGenerator \ No newline at end of file diff --git a/src/bitbishop/moves/rook_move_gen.cpp b/src/bitbishop/moves/rook_move_gen.cpp new file mode 100644 index 0000000..910bacc --- /dev/null +++ b/src/bitbishop/moves/rook_move_gen.cpp @@ -0,0 +1,117 @@ +#include +#include +#include + +void RookMoveGenerator::generate_pseudo_legal_moves(std::vector& moves, const Board& board, Color side) { + Bitboard rooks = board.rooks(side); + Bitboard empty = board.unoccupied(); + Bitboard enemy = board.enemy(side); + Bitboard occupied = board.occupied(); + + // warning: this loop is destructive on Bitboard rooks + while (auto from_opt = rooks.pop_lsb()) { + Square from = from_opt.value(); + + Bitboard rook_attacks; + rook_attacks |= north_ray(from, occupied); + rook_attacks |= south_ray(from, occupied); + rook_attacks |= east_ray(from, occupied); + rook_attacks |= west_ray(from, occupied); + + // Silent moves + Bitboard rook_moves = rook_attacks & empty; + for (auto to : rook_moves) { + moves.emplace_back(from, to, std::nullopt, false, false, false); + } + + // Captures + Bitboard rook_captures = rook_attacks & enemy; + for (auto to : rook_captures) { + moves.emplace_back(from, to, std::nullopt, true, false, false); + } + + // Castling moves (pseudo-legal only checks piece positions, not attacks) + add_rook_castling(moves, from, side, board); + } +} + +void RookMoveGenerator::add_rook_castling(std::vector& moves, Square from, Color side, const Board& board) { + // TODO: These Castling utilities must be moved out of the KingMoveGenerator namespace + // as it is common code with RookMoveGenerator + + // Kingside castling + if (KingMoveGenerator::can_castle_kingside(board, side)) { + Square to = (side == Color::WHITE) ? Squares::F1 : Squares::F8; + moves.emplace_back(from, to, std::nullopt, false, false, true); + } + + // Queenside castling + if (KingMoveGenerator::can_castle_queenside(board, side)) { + Square to = (side == Color::WHITE) ? Squares::D1 : Squares::D8; + moves.emplace_back(from, to, std::nullopt, false, false, true); + } +} + +Bitboard RookMoveGenerator::north_ray(Square from, const Bitboard& occupied) { + Bitboard n_ray = Lookups::ROOK_NORTH_ATTACKS[from.value()]; + Bitboard blockers = n_ray & occupied; + + // Closest blocker (N goes up, so use lsb) + std::optional first_blocker = blockers.lsb(); + + if (first_blocker.has_value()) { + Square blocker_square = first_blocker.value(); + const Bitboard& beyond_blocker = Lookups::ROOK_NORTH_ATTACKS[blocker_square.value()]; + n_ray &= ~beyond_blocker; + } + + return n_ray; +} + +Bitboard RookMoveGenerator::south_ray(Square from, const Bitboard& occupied) { + Bitboard s_ray = Lookups::ROOK_SOUTH_ATTACKS[from.value()]; + Bitboard blockers = s_ray & occupied; + + // Closest blocker (N goes up, so use lsb) + std::optional first_blocker = blockers.lsb(); + + if (first_blocker.has_value()) { + Square blocker_square = first_blocker.value(); + const Bitboard& beyond_blocker = Lookups::ROOK_SOUTH_ATTACKS[blocker_square.value()]; + s_ray &= ~beyond_blocker; + } + + return s_ray; +} + +Bitboard RookMoveGenerator::east_ray(Square from, const Bitboard& occupied) { + Bitboard e_ray = Lookups::ROOK_EAST_ATTACKS[from.value()]; + Bitboard blockers = e_ray & occupied; + + // Closest blocker (N goes up, so use lsb) + std::optional first_blocker = blockers.lsb(); + + if (first_blocker.has_value()) { + Square blocker_square = first_blocker.value(); + const Bitboard& beyond_blocker = Lookups::ROOK_EAST_ATTACKS[blocker_square.value()]; + e_ray &= ~beyond_blocker; + } + + return e_ray; +} + +Bitboard RookMoveGenerator::west_ray(Square from, const Bitboard& occupied) { + Bitboard w_ray = Lookups::ROOK_WEST_ATTACKS[from.value()]; + Bitboard blockers = w_ray & occupied; + + // Closest blocker (N goes up, so use lsb) + std::optional first_blocker = blockers.lsb(); + + if (first_blocker.has_value()) { + Square blocker_square = first_blocker.value(); + const Bitboard& beyond_blocker = Lookups::ROOK_WEST_ATTACKS[blocker_square.value()]; + w_ray &= ~beyond_blocker; + } + + return w_ray; +} From adfd23059ae16fd9fcbaa0c6e7b4768e49496b22 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Sun, 14 Dec 2025 16:30:04 +0100 Subject: [PATCH 3/4] implemented tests for rook pseudo legal moves generation --- src/bitbishop/moves/rook_move_gen.cpp | 20 +- tests/bitbishop/helpers/moves.hpp | 32 +- .../test_rmg_add_rook_castling.cpp | 247 ++++++++ .../test_rmg_east_rays.cpp | 160 +++++ .../test_rmg_generate_pseudo_legal_moves.cpp | 573 ++++++++++++++++++ .../test_rmg_north_rays.cpp | 160 +++++ .../test_rmg_south_rays.cpp | 160 +++++ .../test_rmg_west_rays.cpp | 158 +++++ 8 files changed, 1496 insertions(+), 14 deletions(-) create mode 100644 tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp create mode 100644 tests/bitbishop/moves/rook_move_generator/test_rmg_east_rays.cpp create mode 100644 tests/bitbishop/moves/rook_move_generator/test_rmg_generate_pseudo_legal_moves.cpp create mode 100644 tests/bitbishop/moves/rook_move_generator/test_rmg_north_rays.cpp create mode 100644 tests/bitbishop/moves/rook_move_generator/test_rmg_south_rays.cpp create mode 100644 tests/bitbishop/moves/rook_move_generator/test_rmg_west_rays.cpp diff --git a/src/bitbishop/moves/rook_move_gen.cpp b/src/bitbishop/moves/rook_move_gen.cpp index 910bacc..b2c342f 100644 --- a/src/bitbishop/moves/rook_move_gen.cpp +++ b/src/bitbishop/moves/rook_move_gen.cpp @@ -38,15 +38,19 @@ void RookMoveGenerator::generate_pseudo_legal_moves(std::vector& moves, co void RookMoveGenerator::add_rook_castling(std::vector& moves, Square from, Color side, const Board& board) { // TODO: These Castling utilities must be moved out of the KingMoveGenerator namespace // as it is common code with RookMoveGenerator + Square kingside_rook = (side == Color::WHITE) ? Squares::H1 : Squares::H8; + Square queenside_rook = (side == Color::WHITE) ? Squares::A1 : Squares::A8; // Kingside castling - if (KingMoveGenerator::can_castle_kingside(board, side)) { + const bool can_castle_kingside = KingMoveGenerator::can_castle_kingside(board, side) && (from == kingside_rook); + if (can_castle_kingside) { Square to = (side == Color::WHITE) ? Squares::F1 : Squares::F8; moves.emplace_back(from, to, std::nullopt, false, false, true); } // Queenside castling - if (KingMoveGenerator::can_castle_queenside(board, side)) { + const bool can_castle_queenside = KingMoveGenerator::can_castle_queenside(board, side) && (from == queenside_rook); + if (can_castle_queenside) { Square to = (side == Color::WHITE) ? Squares::D1 : Squares::D8; moves.emplace_back(from, to, std::nullopt, false, false, true); } @@ -56,7 +60,7 @@ Bitboard RookMoveGenerator::north_ray(Square from, const Bitboard& occupied) { Bitboard n_ray = Lookups::ROOK_NORTH_ATTACKS[from.value()]; Bitboard blockers = n_ray & occupied; - // Closest blocker (N goes up, so use lsb) + // Closest blocker (N goes up towards msb, so use lsb) std::optional first_blocker = blockers.lsb(); if (first_blocker.has_value()) { @@ -72,8 +76,8 @@ Bitboard RookMoveGenerator::south_ray(Square from, const Bitboard& occupied) { Bitboard s_ray = Lookups::ROOK_SOUTH_ATTACKS[from.value()]; Bitboard blockers = s_ray & occupied; - // Closest blocker (N goes up, so use lsb) - std::optional first_blocker = blockers.lsb(); + // Closest blocker (S goes down towards lsb, so use msb) + std::optional first_blocker = blockers.msb(); if (first_blocker.has_value()) { Square blocker_square = first_blocker.value(); @@ -88,7 +92,7 @@ Bitboard RookMoveGenerator::east_ray(Square from, const Bitboard& occupied) { Bitboard e_ray = Lookups::ROOK_EAST_ATTACKS[from.value()]; Bitboard blockers = e_ray & occupied; - // Closest blocker (N goes up, so use lsb) + // Closest blocker (E goes twoards msb, so use lsb) std::optional first_blocker = blockers.lsb(); if (first_blocker.has_value()) { @@ -104,8 +108,8 @@ Bitboard RookMoveGenerator::west_ray(Square from, const Bitboard& occupied) { Bitboard w_ray = Lookups::ROOK_WEST_ATTACKS[from.value()]; Bitboard blockers = w_ray & occupied; - // Closest blocker (N goes up, so use lsb) - std::optional first_blocker = blockers.lsb(); + // Closest blocker (W goes towards lsb, so use msb) + std::optional first_blocker = blockers.msb(); if (first_blocker.has_value()) { Square blocker_square = first_blocker.value(); diff --git a/tests/bitbishop/helpers/moves.hpp b/tests/bitbishop/helpers/moves.hpp index bb2073f..d54aafd 100644 --- a/tests/bitbishop/helpers/moves.hpp +++ b/tests/bitbishop/helpers/moves.hpp @@ -173,19 +173,39 @@ int count_quiet_moves(const std::vector& moves) { } /** - * @brief Counts kingside castling moves for the given side. + * @brief Counts kingside castling moves for the king for the given side. */ -int count_kingside_castling(const std::vector& moves, Color side) { +int count_king_kingside_castling(const std::vector& moves, Color side) { + Square from = (side == Color::WHITE) ? Squares::E1 : Squares::E8; Square target = (side == Color::WHITE) ? Squares::G1 : Squares::G8; - return count_if(moves, [&](const Move& m) { return m.is_castling && m.to == target; }); + return count_if(moves, [&](const Move& m) { return m.is_castling && m.to == target && m.from == from; }); } /** - * @brief Counts queenside castling moves for the given side. + * @brief Counts queenside castling moves for the king for the given side. */ -int count_queenside_castling(const std::vector& moves, Color side) { +int count_king_queenside_castling(const std::vector& moves, Color side) { + Square from = (side == Color::WHITE) ? Squares::E1 : Squares::E8; Square target = (side == Color::WHITE) ? Squares::C1 : Squares::C8; - return count_if(moves, [&](const Move& m) { return m.is_castling && m.to == target; }); + return count_if(moves, [&](const Move& m) { return m.is_castling && m.to == target && m.from == from; }); +} + +/** + * @brief Counts kingside castling moves for the rook for the given side. + */ +int count_rook_kingside_castling(const std::vector& moves, Color side) { + Square from = (side == Color::WHITE) ? Squares::H1 : Squares::H8; + Square target = (side == Color::WHITE) ? Squares::F1 : Squares::F8; + return count_if(moves, [&](const Move& m) { return m.is_castling && m.to == target && m.from == from; }); +} + +/** + * @brief Counts queenside castling moves for the rook for the given side. + */ +int count_rook_queenside_castling(const std::vector& moves, Color side) { + Square from = (side == Color::WHITE) ? Squares::A1 : Squares::A8; + Square target = (side == Color::WHITE) ? Squares::D1 : Squares::D8; + return count_if(moves, [&](const Move& m) { return m.is_castling && m.to == target && m.from == from; }); } /** diff --git a/tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp b/tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp new file mode 100644 index 0000000..bb38482 --- /dev/null +++ b/tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp @@ -0,0 +1,247 @@ +#include + +#include +#include +#include +#include +#include + +/** + * @test RookMoveGenerator::add_rook_castling() with no castling rights + * @brief Verifies that no castling moves are added when castling rights are not available. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingNoCastlingRights) { + Board board("8/8/8/8/8/8/8/R3K2R w - - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 0); + + RookMoveGenerator::add_rook_castling(moves, Squares::H1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with no castling rights + * @brief Verifies that no castling moves are added when castling rights are not available. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBlackNoCastlingRights) { + Board board("r3k2r/8/8/8/8/8/8/8 w - - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); + + RookMoveGenerator::add_rook_castling(moves, Squares::H8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with both castling rights available + * @brief Verifies that both kingside and queenside castling moves are added when both are legal. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBothSidesAvailable) { + Board board("8/8/8/8/8/8/8/R3K2R w KQ - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 1); + + RookMoveGenerator::add_rook_castling(moves, Squares::H1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 2); + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 1); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with only kingside castling available + * @brief Verifies that only kingside castling move is added when queenside is not legal. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingOnlyKingsideAvailable) { + Board board("8/8/8/8/8/8/8/R3K2R w K - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 0); + + RookMoveGenerator::add_rook_castling(moves, Squares::H1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 1); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with only queenside castling available + * @brief Verifies that only queenside castling move is added when kingside is not legal. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingOnlyQueensideAvailable) { + Board board("8/8/8/8/8/8/8/R3K2R w Q - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 1); + + RookMoveGenerator::add_rook_castling(moves, Squares::H1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 0); +} + +/** + * @test RookMoveGenerator::add_rook_castling() for black with both sides available + * @brief Verifies that castling moves are correctly generated for black pieces. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBlackBothSidesAvailable) { + Board board("r3k2r/8/8/8/8/8/8/8 b kq - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 1); + + RookMoveGenerator::add_rook_castling(moves, Squares::H8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 2); + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 1); +} + +/** + * @test RookMoveGenerator::add_rook_castling() for black with only kingside available + * @brief Verifies that only kingside castling is added for black when queenside is blocked. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBlackOnlyKingsideAvailable) { + Board board("r3k2r/8/8/8/8/8/8/8 b k - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 0); + + RookMoveGenerator::add_rook_castling(moves, Squares::H8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 1); +} + +/** + * @test RookMoveGenerator::add_rook_castling() for black with only queenside available + * @brief Verifies that only queenside castling is added for black when kingside is blocked. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBlackOnlyQueensideAvailable) { + Board board("r3k2r/8/8/8/8/8/8/8 b q - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 1); + + RookMoveGenerator::add_rook_castling(moves, Squares::H8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 1); + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 0); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with pieces blocking castling + * @brief Verifies that castling moves are not added when pieces block the path. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBlockedByPieces) { + Board board("8/8/8/8/8/8/8/R1B1KN1R w KQ - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 0); + + RookMoveGenerator::add_rook_castling(moves, Squares::H1, Color::WHITE, board); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with pieces blocking castling + * @brief Verifies that castling moves are not added when pieces block the path. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBlackBlockedByPieces) { + Board board("r1b1kn1r/8/8/8/8/8/8/8 w kq - 0 1"); + + std::vector moves; + + RookMoveGenerator::add_rook_castling(moves, Squares::A8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); + + RookMoveGenerator::add_rook_castling(moves, Squares::H8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with invalid starting square for a black rook + * @brief Verifies that castling moves are not added when black rook's starting position is invalid. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingInvalidFromSquare) { + Board board("8/8/8/8/8/8/8/R1B1KN1R w KQ - 0 1"); + + std::vector moves; + + // Should start from square A1 for white pieces + RookMoveGenerator::add_rook_castling(moves, Squares::A8, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::D4, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::E1, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); + + // Should start from square H1 for white pieces + RookMoveGenerator::add_rook_castling(moves, Squares::H8, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::D5, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::E1, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test RookMoveGenerator::add_rook_castling() with invalid starting square for a white rook + * @brief Verifies that castling moves are not added when white rook's starting position is invalid. + */ +TEST(RookMoveGeneratorTest, AddRookCastlingBlackInvalidFromSquare) { + Board board("8/8/8/8/8/8/8/R3K2R w KQ - 0 1"); + + std::vector moves; + + // Should start from square A8 for white pieces + RookMoveGenerator::add_rook_castling(moves, Squares::A1, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::D4, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::E8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); + + // Should start from square H8 for white pieces + RookMoveGenerator::add_rook_castling(moves, Squares::H1, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::D5, Color::BLACK, board); + RookMoveGenerator::add_rook_castling(moves, Squares::E8, Color::BLACK, board); + + EXPECT_EQ(moves.size(), 0); +} diff --git a/tests/bitbishop/moves/rook_move_generator/test_rmg_east_rays.cpp b/tests/bitbishop/moves/rook_move_generator/test_rmg_east_rays.cpp new file mode 100644 index 0000000..d85ce20 --- /dev/null +++ b/tests/bitbishop/moves/rook_move_generator/test_rmg_east_rays.cpp @@ -0,0 +1,160 @@ +#include + +#include +#include +#include + +/** + * @test RookMoveGenerator::east_ray() with no blockers + * @brief Verifies that east_ray returns all squares to the east when unobstructed. + */ +TEST(RookMoveGeneratorTest, EastRayNoBlockers) { + Bitboard occupied; + Square from = Squares::A4; // Left side of board, rank 4 + Bitboard result = RookMoveGenerator::east_ray(from, occupied); + + // Should include: B4, C4, D4, E4, F4, G4, H4 + EXPECT_TRUE(result.test(Square::B4)); + EXPECT_TRUE(result.test(Square::C4)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_TRUE(result.test(Square::E4)); + EXPECT_TRUE(result.test(Square::F4)); + EXPECT_TRUE(result.test(Square::G4)); + EXPECT_TRUE(result.test(Square::H4)); + EXPECT_EQ(result.count(), 7); +} + +/** + * @test RookMoveGenerator::east_ray() with a blocker + * @brief Verifies that east_ray stops at the first blocker (inclusive). + */ +TEST(RookMoveGeneratorTest, EastRayWithBlocker) { + Bitboard occupied; + occupied.set(Square::E4); // Blocker at E4 + Square from = Squares::A4; + Bitboard result = RookMoveGenerator::east_ray(from, occupied); + + // Should include: B4, C4, D4, E4 (blocker), but not F4, G4, H4 + EXPECT_TRUE(result.test(Square::B4)); + EXPECT_TRUE(result.test(Square::C4)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_TRUE(result.test(Square::E4)); // Blocker is included + EXPECT_FALSE(result.test(Square::F4)); + EXPECT_FALSE(result.test(Square::G4)); + EXPECT_FALSE(result.test(Square::H4)); + EXPECT_EQ(result.count(), 4); +} + +/** + * @test RookMoveGenerator::east_ray() with immediate blocker + * @brief Verifies that east_ray handles a blocker on the first square. + */ +TEST(RookMoveGeneratorTest, EastRayWithImmediateBlocker) { + Bitboard occupied; + occupied.set(Square::B4); // Immediate blocker + Square from = Squares::A4; + Bitboard result = RookMoveGenerator::east_ray(from, occupied); + + // Should only include B4 + EXPECT_TRUE(result.test(Square::B4)); + EXPECT_FALSE(result.test(Square::C4)); + EXPECT_EQ(result.count(), 1); +} + +/** + * @test RookMoveGenerator::east_ray() with multiple blockers + * @brief Verifies that east_ray stops at the FIRST blocker, not subsequent ones. + */ +TEST(RookMoveGeneratorTest, EastRayStopAtFirstBlockerOnly) { + Bitboard occupied; + occupied.set(Square::D4); // First blocker + occupied.set(Square::G4); // Second blocker (should be ignored) + Square from = Squares::A4; + Bitboard result = RookMoveGenerator::east_ray(from, occupied); + + // Should include: B4, C4, D4 (first blocker), but not E4, F4, G4, H4 + EXPECT_TRUE(result.test(Square::B4)); + EXPECT_TRUE(result.test(Square::C4)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_FALSE(result.test(Square::E4)); + EXPECT_FALSE(result.test(Square::F4)); + EXPECT_FALSE(result.test(Square::G4)); + EXPECT_EQ(result.count(), 3); +} + +/** + * @test RookMoveGenerator::east_ray() from H-file edge squares + * @brief Verifies that east_ray returns empty bitboard when starting from the east edge + * where no moves are possible in that direction. + */ +TEST(RookMoveGeneratorTest, EastRayFromHFileEdgeSquares) { + Bitboard occupied; + + // East from H1 (rightmost column, rank 1) - no squares available + Bitboard east_result = RookMoveGenerator::east_ray(Squares::H1, occupied); + EXPECT_EQ(east_result.count(), 0); + + // East from H8 (rightmost column, rank 8) - no squares available + east_result = RookMoveGenerator::east_ray(Squares::H8, occupied); + EXPECT_EQ(east_result.count(), 0); +} + +/** + * @test RookMoveGenerator::east_ray() from H-file with different ranks + * @brief Verifies that east_ray returns empty bitboard from any H-file square. + */ +TEST(RookMoveGeneratorTest, EastRayFromHFileDifferentRanks) { + Bitboard occupied; + + // East from H4 (rightmost column, rank 4) - no squares available + Bitboard east_result = RookMoveGenerator::east_ray(Squares::H4, occupied); + EXPECT_EQ(east_result.count(), 0); + + // East from H5 (rightmost column, rank 5) - no squares available + east_result = RookMoveGenerator::east_ray(Squares::H5, occupied); + EXPECT_EQ(east_result.count(), 0); +} + +/** + * @test RookMoveGenerator::east_ray() from center board positions + * @brief Verifies that east_ray correctly handles starting positions in the middle of the board. + */ +TEST(RookMoveGeneratorTest, EastRayFromCenterPositions) { + Bitboard occupied; + + // From D4 (middle of board) + Bitboard result = RookMoveGenerator::east_ray(Squares::D4, occupied); + EXPECT_TRUE(result.test(Square::E4)); + EXPECT_TRUE(result.test(Square::F4)); + EXPECT_TRUE(result.test(Square::G4)); + EXPECT_TRUE(result.test(Square::H4)); + EXPECT_EQ(result.count(), 4); + + // From C5 (also middle of board) + result = RookMoveGenerator::east_ray(Squares::C5, occupied); + EXPECT_TRUE(result.test(Square::D5)); + EXPECT_TRUE(result.test(Square::E5)); + EXPECT_TRUE(result.test(Square::F5)); + EXPECT_TRUE(result.test(Square::G5)); + EXPECT_TRUE(result.test(Square::H5)); + EXPECT_EQ(result.count(), 5); +} + +/** + * @test RookMoveGenerator::east_ray() from near-edge positions + * @brief Verifies that east_ray correctly handles starting positions near the eastern edge. + */ +TEST(RookMoveGeneratorTest, EastRayFromNearEdgePositions) { + Bitboard occupied; + + // From G1 (one square from edge) + Bitboard result = RookMoveGenerator::east_ray(Squares::G1, occupied); + EXPECT_TRUE(result.test(Square::H1)); + EXPECT_EQ(result.count(), 1); + + // From F3 (two squares from edge) + result = RookMoveGenerator::east_ray(Squares::F3, occupied); + EXPECT_TRUE(result.test(Square::G3)); + EXPECT_TRUE(result.test(Square::H3)); + EXPECT_EQ(result.count(), 2); +} \ No newline at end of file diff --git a/tests/bitbishop/moves/rook_move_generator/test_rmg_generate_pseudo_legal_moves.cpp b/tests/bitbishop/moves/rook_move_generator/test_rmg_generate_pseudo_legal_moves.cpp new file mode 100644 index 0000000..ca1e8d9 --- /dev/null +++ b/tests/bitbishop/moves/rook_move_generator/test_rmg_generate_pseudo_legal_moves.cpp @@ -0,0 +1,573 @@ +#include + +#include +#include +#include + +/** + * @brief Test fixture for rook pseudo-legal move generation. + */ +class RookPseudoLegalMovesTest : public ::testing::Test { + protected: + std::vector moves; + + void SetUp() override {} + + void TearDown() override {} +}; + +/** + * @test Verifies White rooks have 0 moves from starting position (blocked by own pieces). + */ +TEST_F(RookPseudoLegalMovesTest, StartingPositionWhiteHas0Moves) { + Board board; + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Verifies Black rooks have 0 moves from starting position (blocked by own pieces). + */ +TEST_F(RookPseudoLegalMovesTest, StartingPositionBlackHas0Moves) { + Board board; + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Verifies White rooks have no captures from starting position. + */ +TEST_F(RookPseudoLegalMovesTest, StartingPositionWhiteHasNoCaptures) { + Board board; + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies Black rooks have no captures from starting position. + */ +TEST_F(RookPseudoLegalMovesTest, StartingPositionBlackHasNoCaptures) { + Board board; + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies White rook on empty board generates 14 moves (7 in each direction). + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookCenterEmptyBoardHas14Moves) { + Board board("8/8/8/8/3R4/8/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 14); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies Black rook on empty board generates 14 moves (7 in each direction). + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookCenterEmptyBoardHas14Moves) { + Board board("8/8/8/8/3r4/8/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 14); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies White rook in corner has 14 moves on empty board. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookInCornerHas14Moves) { + Board board("8/8/8/8/8/8/8/R7 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 14); + // Should have 7 moves north and 7 moves east + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies Black rook in corner has 14 moves on empty board. + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookInCornerHas14Moves) { + Board board("r7/8/8/8/8/8/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 14); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies White rook can capture enemy pieces in all four directions. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookCanCaptureEnemyPieces) { + Board board("8/8/8/3p4/2pRp3/3p4/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 4); + EXPECT_EQ(count_captures(moves), 4); +} + +/** + * @test Verifies Black rook can capture enemy pieces in all four directions. + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookCanCaptureEnemyPieces) { + Board board("8/8/8/3P4/2PrP3/3P4/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 4); + EXPECT_EQ(count_captures(moves), 4); +} + +/** + * @test Verifies White rook cannot capture own pieces. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookCannotCaptureOwnPieces) { + Board board("8/8/8/3P4/2PRP3/3P4/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies Black rook cannot capture own pieces. + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookCannotCaptureOwnPieces) { + Board board("8/8/8/3p4/2prp3/3p4/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 0); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies White rook with mix of blocked and available squares. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookMixedOccupancy) { + Board board("8/8/8/3p4/2PRp3/3P4/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_captures(moves), 2); + EXPECT_EQ(moves.size(), 2); +} + +/** + * @test Verifies Black rook with mix of blocked and available squares. + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookMixedOccupancy) { + Board board("8/8/8/3P4/2prP3/3p4/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_captures(moves), 2); + EXPECT_EQ(moves.size(), 2); +} + +/** + * @test Verifies White rook moves along rank are generated correctly. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookMovesAlongRank) { + Board board("8/8/8/8/R7/8/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 14); + + EXPECT_TRUE(contains_move(moves, {Squares::A4, Squares::B4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A4, Squares::H4, std::nullopt, false, false, false})); +} + +/** + * @test Verifies Black rook moves along rank are generated correctly. + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookMovesAlongRank) { + Board board("8/8/8/8/r7/8/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 14); + EXPECT_TRUE(contains_move(moves, {Squares::A4, Squares::B4, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A4, Squares::H4, std::nullopt, false, false, false})); +} + +/** + * @test Verifies White rook moves along file are generated correctly. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookMovesAlongFile) { + Board board("8/8/8/8/8/8/8/R7 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 14); + + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A2, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A1, Squares::A8, std::nullopt, false, false, false})); +} + +/** + * @test Verifies Black rook moves along file are generated correctly. + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookMovesAlongFile) { + Board board("r7/8/8/8/8/8/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 14); + EXPECT_TRUE(contains_move(moves, {Squares::A8, Squares::A7, std::nullopt, false, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::A8, Squares::A1, std::nullopt, false, false, false})); +} + +/** + * @test Verifies White rook stops at first blocker in each direction. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteRookStopsAtBlocker) { + Board board("3p4/3p4/3p4/8/pp1R1ppp/8/3p4/3p4 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 8); + 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::D2, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, true, false, false})); +} + +/** + * @test Verifies Black rook stops at first blocker in each direction. + */ +TEST_F(RookPseudoLegalMovesTest, BlackRookStopsAtBlocker) { + Board board("8/8/3P4/8/PP1r1PPP/8/3P4/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(moves.size(), 8); + 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::D2, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::B4, std::nullopt, true, false, false})); + EXPECT_TRUE(contains_move(moves, {Squares::D4, Squares::F4, std::nullopt, true, false, false})); +} + +/** + * @test Verifies multiple White rooks generate moves independently. + */ +TEST_F(RookPseudoLegalMovesTest, MultipleWhiteRooksGenerateMoves) { + Board board("8/8/8/8/R6R/8/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + // Each rook has 13 moves, total 26 + EXPECT_EQ(moves.size(), 26); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies multiple Black rooks generate moves independently. + */ +TEST_F(RookPseudoLegalMovesTest, MultipleBlackRooksGenerateMoves) { + Board board("8/8/8/8/r6r/8/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + // Each rook has 13 moves, total 26 + EXPECT_EQ(moves.size(), 26); + EXPECT_EQ(count_captures(moves), 0); +} + +/** + * @test Verifies White kingside castling is available from starting position. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteBothCastlingAvailable) { + Board board("8/8/8/8/8/8/8/R3K2R w KQ - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 1); +} + +/** + * @test Verifies Black kingside castling is available from starting position. + */ +TEST_F(RookPseudoLegalMovesTest, BlackBothCastlingAvailable) { + Board board("r3k2r/8/8/8/8/8/8/8 b kq - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 1); +} + +/** + * @test Verifies White castling unavailable when castling rights are lost. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteCastlingUnavailableWithoutRights) { + Board board("8/8/8/8/8/8/8/R3K2R w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 0); +} + +/** + * @test Verifies Black castling unavailable when castling rights are lost. + */ +TEST_F(RookPseudoLegalMovesTest, BlackCastlingUnavailableWithoutRights) { + Board board("r3k2r/8/8/8/8/8/8/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 0); +} + +/** + * @test Verifies White can only castle kingside when queenside rights are lost. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteOnlyKingsideCastlingAvailable) { + Board board("8/8/8/8/8/8/8/R3K2R w K - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 0); +} + +/** + * @test Verifies White can only castle queenside when kingside rights are lost. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteOnlyQueensideCastlingAvailable) { + Board board("8/8/8/8/8/8/8/R3K2R w Q - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 1); +} + +/** + * @test Verifies Black can only castle kingside when queenside rights are lost. + */ +TEST_F(RookPseudoLegalMovesTest, BlackOnlyKingsideCastlingAvailable) { + Board board("r3k2r/8/8/8/8/8/8/8 b k - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 1); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 0); +} + +/** + * @test Verifies Black can only castle queenside when kingside rights are lost. + */ +TEST_F(RookPseudoLegalMovesTest, BlackOnlyQueensideCastlingAvailable) { + Board board("r3k2r/8/8/8/8/8/8/8 b q - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 1); +} + +/** + * @test Verifies White castling blocked when pieces obstruct the path. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteCastlingBlockedByPieces) { + Board board("8/8/8/8/8/8/8/R1B1KN1R w KQ - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::WHITE), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::WHITE), 0); +} + +/** + * @test Verifies Black castling blocked when pieces obstruct the path. + */ +TEST_F(RookPseudoLegalMovesTest, BlackCastlingBlockedByPieces) { + Board board("r1b1kn1r/8/8/8/8/8/8/8 b kq - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + EXPECT_EQ(count_rook_kingside_castling(moves, Color::BLACK), 0); + EXPECT_EQ(count_rook_queenside_castling(moves, Color::BLACK), 0); +} + +/** + * @test Verifies all rook moves have no promotion flag. + */ +TEST_F(RookPseudoLegalMovesTest, AllMovesHaveNoPromotionFlag) { + Board board("8/8/8/8/3R4/8/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(count_promotions(moves), 0); +} + +/** + * @test Verifies all rook moves have no en passant flag. + */ +TEST_F(RookPseudoLegalMovesTest, AllMovesHaveNoEnPassantFlag) { + Board board("8/8/8/8/3R4/8/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + for (const auto& move : moves) { + EXPECT_FALSE(move.is_en_passant) << "Rook moves should never be en passant"; + } +} + +/** + * @test Verifies only castling moves have castling flag set. + */ +TEST_F(RookPseudoLegalMovesTest, OnlyCastlingMovesHaveCastlingFlag) { + Board board("8/8/8/8/8/8/8/R3K2R w KQ - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + for (const auto& move : moves) { + if (move.is_castling) { + EXPECT_TRUE(move.is_castling); + } else { + EXPECT_FALSE(move.is_castling); + } + } +} + +/** + * @test Verifies complex position with captures, regular moves, and castling. + */ +TEST_F(RookPseudoLegalMovesTest, ComplexPositionMixedMoves) { + Board board("8/8/3p4/8/ppppRppp/8/4P3/8 b - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 7); + EXPECT_EQ(count_captures(moves), 2); + EXPECT_EQ(count_quiet_moves(moves), 5); +} + +/** + * @test Verifies rook with long-range captures. + */ +TEST_F(RookPseudoLegalMovesTest, RookLongRangeCapture) { + Board board("8/8/8/8/R6p/8/8/8 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + // Should be able to capture on H4 + EXPECT_TRUE(contains_move(moves, {Squares::A4, Squares::H4, std::nullopt, true, false, false})); +} + +/** + * @test Verifies no moves generated when no rooks present. + */ +TEST_F(RookPseudoLegalMovesTest, NoMovesWhenNoRooks) { + Board board("8/8/8/8/8/8/8/4K3 w - - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + EXPECT_EQ(moves.size(), 0); +} + +/** + * @test Verifies castling moves have correct properties for White kingside. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteKingsideCastlingCorrectProperties) { + Board board("8/8/8/8/8/8/8/R3K2R w KQ - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + bool found_kingside = false; + for (const auto& move : moves) { + if (move.is_castling && move.from == Squares::H1 && move.to == Squares::F1) { + EXPECT_FALSE(move.is_capture); + EXPECT_FALSE(move.is_en_passant); + EXPECT_FALSE(move.promotion.has_value()); + found_kingside = true; + } + } + EXPECT_TRUE(found_kingside) << "White kingside castling move should be present"; +} + +/** + * @test Verifies castling moves have correct properties for White queenside. + */ +TEST_F(RookPseudoLegalMovesTest, WhiteQueensideCastlingCorrectProperties) { + Board board("8/8/8/8/8/8/8/R3K2R w KQ - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::WHITE); + + bool found_queenside = false; + for (const auto& move : moves) { + if (move.is_castling && move.from == Squares::A1 && move.to == Squares::D1) { + EXPECT_FALSE(move.is_capture); + EXPECT_FALSE(move.is_en_passant); + EXPECT_FALSE(move.promotion.has_value()); + found_queenside = true; + } + } + EXPECT_TRUE(found_queenside) << "White queenside castling move should be present"; +} + +/** + * @test Verifies castling moves have correct properties for Black kingside. + */ +TEST_F(RookPseudoLegalMovesTest, BlackKingsideCastlingCorrectProperties) { + Board board("r3k2r/8/8/8/8/8/8/8 b kq - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + bool found_kingside = false; + for (const auto& move : moves) { + if (move.is_castling && move.from == Squares::H8 && move.to == Squares::F8) { + EXPECT_FALSE(move.is_capture); + EXPECT_FALSE(move.is_en_passant); + EXPECT_FALSE(move.promotion.has_value()); + found_kingside = true; + } + } + EXPECT_TRUE(found_kingside) << "Black kingside castling move should be present"; +} + +/** + * @test Verifies castling moves have correct properties for Black queenside. + */ +TEST_F(RookPseudoLegalMovesTest, BlackQueensideCastlingCorrectProperties) { + Board board("r3k2r/8/8/8/8/8/8/8 b kq - 0 1"); + + RookMoveGenerator::generate_pseudo_legal_moves(moves, board, Color::BLACK); + + bool found_queenside = false; + for (const auto& move : moves) { + if (move.is_castling && move.from == Squares::A8 && move.to == Squares::D8) { + EXPECT_FALSE(move.is_capture); + EXPECT_FALSE(move.is_en_passant); + EXPECT_FALSE(move.promotion.has_value()); + found_queenside = true; + } + } + EXPECT_TRUE(found_queenside) << "Black queenside castling move should be present"; +} diff --git a/tests/bitbishop/moves/rook_move_generator/test_rmg_north_rays.cpp b/tests/bitbishop/moves/rook_move_generator/test_rmg_north_rays.cpp new file mode 100644 index 0000000..d24d089 --- /dev/null +++ b/tests/bitbishop/moves/rook_move_generator/test_rmg_north_rays.cpp @@ -0,0 +1,160 @@ +#include + +#include +#include +#include + +/** + * @test RookMoveGenerator::north_ray() with no blockers + * @brief Verifies that north_ray returns all squares to the north when unobstructed. + */ +TEST(RookMoveGeneratorTest, NorthRayNoBlockers) { + Bitboard occupied; + Square from = Squares::D1; // Bottom of board, file D + Bitboard result = RookMoveGenerator::north_ray(from, occupied); + + // Should include: D2, D3, D4, D5, D6, D7, D8 + EXPECT_TRUE(result.test(Square::D2)); + EXPECT_TRUE(result.test(Square::D3)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_TRUE(result.test(Square::D5)); + EXPECT_TRUE(result.test(Square::D6)); + EXPECT_TRUE(result.test(Square::D7)); + EXPECT_TRUE(result.test(Square::D8)); + EXPECT_EQ(result.count(), 7); +} + +/** + * @test RookMoveGenerator::north_ray() with a blocker + * @brief Verifies that north_ray stops at the first blocker (inclusive). + */ +TEST(RookMoveGeneratorTest, NorthRayWithBlocker) { + Bitboard occupied; + occupied.set(Square::D5); // Blocker at D5 + Square from = Squares::D1; + Bitboard result = RookMoveGenerator::north_ray(from, occupied); + + // Should include: D2, D3, D4, D5 (blocker), but not D6, D7, D8 + EXPECT_TRUE(result.test(Square::D2)); + EXPECT_TRUE(result.test(Square::D3)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_TRUE(result.test(Square::D5)); // Blocker is included + EXPECT_FALSE(result.test(Square::D6)); + EXPECT_FALSE(result.test(Square::D7)); + EXPECT_FALSE(result.test(Square::D8)); + EXPECT_EQ(result.count(), 4); +} + +/** + * @test RookMoveGenerator::north_ray() with immediate blocker + * @brief Verifies that north_ray handles a blocker on the first square. + */ +TEST(RookMoveGeneratorTest, NorthRayWithImmediateBlocker) { + Bitboard occupied; + occupied.set(Square::D2); // Immediate blocker + Square from = Squares::D1; + Bitboard result = RookMoveGenerator::north_ray(from, occupied); + + // Should only include D2 + EXPECT_TRUE(result.test(Square::D2)); + EXPECT_FALSE(result.test(Square::D3)); + EXPECT_EQ(result.count(), 1); +} + +/** + * @test RookMoveGenerator::north_ray() with multiple blockers + * @brief Verifies that north_ray stops at the FIRST blocker, not subsequent ones. + */ +TEST(RookMoveGeneratorTest, NorthRayStopAtFirstBlockerOnly) { + Bitboard occupied; + occupied.set(Square::D4); // First blocker + occupied.set(Square::D7); // Second blocker (should be ignored) + Square from = Squares::D1; + Bitboard result = RookMoveGenerator::north_ray(from, occupied); + + // Should include: D2, D3, D4 (first blocker), but not D5, D6, D7, D8 + EXPECT_TRUE(result.test(Square::D2)); + EXPECT_TRUE(result.test(Square::D3)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_FALSE(result.test(Square::D5)); + EXPECT_FALSE(result.test(Square::D6)); + EXPECT_FALSE(result.test(Square::D7)); + EXPECT_EQ(result.count(), 3); +} + +/** + * @test RookMoveGenerator::north_ray() from rank 8 edge squares + * @brief Verifies that north_ray returns empty bitboard when starting from the north edge + * where no moves are possible in that direction. + */ +TEST(RookMoveGeneratorTest, NorthRayFromRank8EdgeSquares) { + Bitboard occupied; + + // North from A8 (top rank, file A) - no squares available + Bitboard north_result = RookMoveGenerator::north_ray(Squares::A8, occupied); + EXPECT_EQ(north_result.count(), 0); + + // North from H8 (top rank, file H) - no squares available + north_result = RookMoveGenerator::north_ray(Squares::H8, occupied); + EXPECT_EQ(north_result.count(), 0); +} + +/** + * @test RookMoveGenerator::north_ray() from rank 8 with different files + * @brief Verifies that north_ray returns empty bitboard from any rank 8 square. + */ +TEST(RookMoveGeneratorTest, NorthRayFromRank8DifferentFiles) { + Bitboard occupied; + + // North from D8 (top rank, file D) - no squares available + Bitboard north_result = RookMoveGenerator::north_ray(Squares::D8, occupied); + EXPECT_EQ(north_result.count(), 0); + + // North from E8 (top rank, file E) - no squares available + north_result = RookMoveGenerator::north_ray(Squares::E8, occupied); + EXPECT_EQ(north_result.count(), 0); +} + +/** + * @test RookMoveGenerator::north_ray() from center board positions + * @brief Verifies that north_ray correctly handles starting positions in the middle of the board. + */ +TEST(RookMoveGeneratorTest, NorthRayFromCenterPositions) { + Bitboard occupied; + + // From D4 (middle of board) + Bitboard result = RookMoveGenerator::north_ray(Squares::D4, occupied); + EXPECT_TRUE(result.test(Square::D5)); + EXPECT_TRUE(result.test(Square::D6)); + EXPECT_TRUE(result.test(Square::D7)); + EXPECT_TRUE(result.test(Square::D8)); + EXPECT_EQ(result.count(), 4); + + // From E3 (also middle of board) + result = RookMoveGenerator::north_ray(Squares::E3, occupied); + EXPECT_TRUE(result.test(Square::E4)); + EXPECT_TRUE(result.test(Square::E5)); + EXPECT_TRUE(result.test(Square::E6)); + EXPECT_TRUE(result.test(Square::E7)); + EXPECT_TRUE(result.test(Square::E8)); + EXPECT_EQ(result.count(), 5); +} + +/** + * @test RookMoveGenerator::north_ray() from near-edge positions + * @brief Verifies that north_ray correctly handles starting positions near the northern edge. + */ +TEST(RookMoveGeneratorTest, NorthRayFromNearEdgePositions) { + Bitboard occupied; + + // From D7 (one square from edge) + Bitboard result = RookMoveGenerator::north_ray(Squares::D7, occupied); + EXPECT_TRUE(result.test(Square::D8)); + EXPECT_EQ(result.count(), 1); + + // From C6 (two squares from edge) + result = RookMoveGenerator::north_ray(Squares::C6, occupied); + EXPECT_TRUE(result.test(Square::C7)); + EXPECT_TRUE(result.test(Square::C8)); + EXPECT_EQ(result.count(), 2); +} \ No newline at end of file diff --git a/tests/bitbishop/moves/rook_move_generator/test_rmg_south_rays.cpp b/tests/bitbishop/moves/rook_move_generator/test_rmg_south_rays.cpp new file mode 100644 index 0000000..f5e0856 --- /dev/null +++ b/tests/bitbishop/moves/rook_move_generator/test_rmg_south_rays.cpp @@ -0,0 +1,160 @@ +#include + +#include +#include +#include + +/** + * @test RookMoveGenerator::south_ray() with no blockers + * @brief Verifies that south_ray returns all squares to the south when unobstructed. + */ +TEST(RookMoveGeneratorTest, SouthRayNoBlockers) { + Bitboard occupied; + Square from = Squares::D8; // Top of board, file D + Bitboard result = RookMoveGenerator::south_ray(from, occupied); + + // Should include: D7, D6, D5, D4, D3, D2, D1 + EXPECT_TRUE(result.test(Square::D7)); + EXPECT_TRUE(result.test(Square::D6)); + EXPECT_TRUE(result.test(Square::D5)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_TRUE(result.test(Square::D3)); + EXPECT_TRUE(result.test(Square::D2)); + EXPECT_TRUE(result.test(Square::D1)); + EXPECT_EQ(result.count(), 7); +} + +/** + * @test RookMoveGenerator::south_ray() with a blocker + * @brief Verifies that south_ray stops at the first blocker (inclusive). + */ +TEST(RookMoveGeneratorTest, SouthRayWithBlocker) { + Bitboard occupied; + occupied.set(Square::D4); // Blocker at D4 + Square from = Squares::D8; + Bitboard result = RookMoveGenerator::south_ray(from, occupied); + + // Should include: D7, D6, D5, D4 (blocker), but not D3, D2, D1 + EXPECT_TRUE(result.test(Square::D7)); + EXPECT_TRUE(result.test(Square::D6)); + EXPECT_TRUE(result.test(Square::D5)); + EXPECT_TRUE(result.test(Square::D4)); // Blocker is included + EXPECT_FALSE(result.test(Square::D3)); + EXPECT_FALSE(result.test(Square::D2)); + EXPECT_FALSE(result.test(Square::D1)); + EXPECT_EQ(result.count(), 4); +} + +/** + * @test RookMoveGenerator::south_ray() with immediate blocker + * @brief Verifies that south_ray handles a blocker on the first square. + */ +TEST(RookMoveGeneratorTest, SouthRayWithImmediateBlocker) { + Bitboard occupied; + occupied.set(Square::D7); // Immediate blocker + Square from = Squares::D8; + Bitboard result = RookMoveGenerator::south_ray(from, occupied); + + // Should only include D7 + EXPECT_TRUE(result.test(Square::D7)); + EXPECT_FALSE(result.test(Square::D6)); + EXPECT_EQ(result.count(), 1); +} + +/** + * @test RookMoveGenerator::south_ray() with multiple blockers + * @brief Verifies that south_ray stops at the FIRST blocker, not subsequent ones. + */ +TEST(RookMoveGeneratorTest, SouthRayStopAtFirstBlockerOnly) { + Bitboard occupied; + occupied.set(Square::D5); // First blocker + occupied.set(Square::D2); // Second blocker (should be ignored) + Square from = Squares::D8; + Bitboard result = RookMoveGenerator::south_ray(from, occupied); + + // Should include: D7, D6, D5 (first blocker), but not D4, D3, D2, D1 + EXPECT_TRUE(result.test(Square::D7)); + EXPECT_TRUE(result.test(Square::D6)); + EXPECT_TRUE(result.test(Square::D5)); + EXPECT_FALSE(result.test(Square::D4)); + EXPECT_FALSE(result.test(Square::D3)); + EXPECT_FALSE(result.test(Square::D2)); + EXPECT_EQ(result.count(), 3); +} + +/** + * @test RookMoveGenerator::south_ray() from rank 1 edge squares + * @brief Verifies that south_ray returns empty bitboard when starting from the south edge + * where no moves are possible in that direction. + */ +TEST(RookMoveGeneratorTest, SouthRayFromRank1EdgeSquares) { + Bitboard occupied; + + // South from A1 (bottom rank, file A) - no squares available + Bitboard south_result = RookMoveGenerator::south_ray(Squares::A1, occupied); + EXPECT_EQ(south_result.count(), 0); + + // South from H1 (bottom rank, file H) - no squares available + south_result = RookMoveGenerator::south_ray(Squares::H1, occupied); + EXPECT_EQ(south_result.count(), 0); +} + +/** + * @test RookMoveGenerator::south_ray() from rank 1 with different files + * @brief Verifies that south_ray returns empty bitboard from any rank 1 square. + */ +TEST(RookMoveGeneratorTest, SouthRayFromRank1DifferentFiles) { + Bitboard occupied; + + // South from D1 (bottom rank, file D) - no squares available + Bitboard south_result = RookMoveGenerator::south_ray(Squares::D1, occupied); + EXPECT_EQ(south_result.count(), 0); + + // South from E1 (bottom rank, file E) - no squares available + south_result = RookMoveGenerator::south_ray(Squares::E1, occupied); + EXPECT_EQ(south_result.count(), 0); +} + +/** + * @test RookMoveGenerator::south_ray() from center board positions + * @brief Verifies that south_ray correctly handles starting positions in the middle of the board. + */ +TEST(RookMoveGeneratorTest, SouthRayFromCenterPositions) { + Bitboard occupied; + + // From D5 (middle of board) + Bitboard result = RookMoveGenerator::south_ray(Squares::D5, occupied); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_TRUE(result.test(Square::D3)); + EXPECT_TRUE(result.test(Square::D2)); + EXPECT_TRUE(result.test(Square::D1)); + EXPECT_EQ(result.count(), 4); + + // From E6 (also middle of board) + result = RookMoveGenerator::south_ray(Squares::E6, occupied); + EXPECT_TRUE(result.test(Square::E5)); + EXPECT_TRUE(result.test(Square::E4)); + EXPECT_TRUE(result.test(Square::E3)); + EXPECT_TRUE(result.test(Square::E2)); + EXPECT_TRUE(result.test(Square::E1)); + EXPECT_EQ(result.count(), 5); +} + +/** + * @test RookMoveGenerator::south_ray() from near-edge positions + * @brief Verifies that south_ray correctly handles starting positions near the southern edge. + */ +TEST(RookMoveGeneratorTest, SouthRayFromNearEdgePositions) { + Bitboard occupied; + + // From D2 (one square from edge) + Bitboard result = RookMoveGenerator::south_ray(Squares::D2, occupied); + EXPECT_TRUE(result.test(Square::D1)); + EXPECT_EQ(result.count(), 1); + + // From C3 (two squares from edge) + result = RookMoveGenerator::south_ray(Squares::C3, occupied); + EXPECT_TRUE(result.test(Square::C2)); + EXPECT_TRUE(result.test(Square::C1)); + EXPECT_EQ(result.count(), 2); +} diff --git a/tests/bitbishop/moves/rook_move_generator/test_rmg_west_rays.cpp b/tests/bitbishop/moves/rook_move_generator/test_rmg_west_rays.cpp new file mode 100644 index 0000000..3543869 --- /dev/null +++ b/tests/bitbishop/moves/rook_move_generator/test_rmg_west_rays.cpp @@ -0,0 +1,158 @@ +#include + +#include +#include +#include + +/** + * @test RookMoveGenerator::west_ray() with no blockers + * @brief Verifies that west_ray returns all squares to the west when unobstructed. + */ +TEST(RookMoveGeneratorTest, WestRayNoBlockers) { + Bitboard occupied; + Square from = Squares::H4; // Right side of board, rank 4 + Bitboard result = RookMoveGenerator::west_ray(from, occupied); + + // Should include: G4, F4, E4, D4, C4, B4, A4 + EXPECT_TRUE(result.test(Square::G4)); + EXPECT_TRUE(result.test(Square::F4)); + EXPECT_TRUE(result.test(Square::E4)); + EXPECT_TRUE(result.test(Square::D4)); + EXPECT_TRUE(result.test(Square::C4)); + EXPECT_TRUE(result.test(Square::B4)); + EXPECT_TRUE(result.test(Square::A4)); + EXPECT_EQ(result.count(), 7); +} + +/** + * @test RookMoveGenerator::west_ray() with a blocker + * @brief Verifies that west_ray stops at the first blocker (inclusive). + */ +TEST(RookMoveGeneratorTest, WestRayWithBlocker) { + Bitboard occupied; + occupied.set(Square::D4); // Blocker at D4 + Square from = Squares::H4; + Bitboard result = RookMoveGenerator::west_ray(from, occupied); + + // Should include: G4, F4, E4, D4 (blocker), but not C4, B4, A4 + EXPECT_TRUE(result.test(Square::G4)); + EXPECT_TRUE(result.test(Square::F4)); + EXPECT_TRUE(result.test(Square::E4)); + EXPECT_TRUE(result.test(Square::D4)); // Blocker is included + EXPECT_FALSE(result.test(Square::C4)); + EXPECT_FALSE(result.test(Square::B4)); + EXPECT_FALSE(result.test(Square::A4)); + EXPECT_EQ(result.count(), 4); +} + +/** + * @test RookMoveGenerator::west_ray() with immediate blocker + * @brief Verifies that west_ray handles a blocker on the first square. + */ +TEST(RookMoveGeneratorTest, WestRayWithImmediateBlocker) { + Bitboard occupied; + occupied.set(Square::G4); // Immediate blocker + Square from = Squares::H4; + Bitboard result = RookMoveGenerator::west_ray(from, occupied); + + // Should only include G4 + EXPECT_TRUE(result.test(Square::G4)); + EXPECT_FALSE(result.test(Square::F4)); + EXPECT_EQ(result.count(), 1); +} + +/** + * @test RookMoveGenerator::west_ray() with multiple blockers + * @brief Verifies that west_ray stops at the FIRST blocker, not subsequent ones. + */ +TEST(RookMoveGeneratorTest, WestRayStopAtFirstBlockerOnly) { + Bitboard occupied; + occupied.set(Square::E4); // First blocker + occupied.set(Square::B4); // Second blocker (should be ignored) + Square from = Squares::H4; + Bitboard result = RookMoveGenerator::west_ray(from, occupied); + + // Should include: G4, F4, E4 (first blocker), but not D4, C4, B4, A4 + EXPECT_TRUE(result.test(Square::G4)); + EXPECT_TRUE(result.test(Square::F4)); + EXPECT_TRUE(result.test(Square::E4)); + EXPECT_FALSE(result.test(Square::D4)); + EXPECT_FALSE(result.test(Square::C4)); + EXPECT_FALSE(result.test(Square::B4)); + EXPECT_EQ(result.count(), 3); +} + +/** + * @test RookMoveGenerator::west_ray() from A-file edge squares + * @brief Verifies that west_ray returns empty bitboard when starting from the west edge + * where no moves are possible in that direction. + */ +TEST(RookMoveGeneratorTest, WestRayFromAFileEdgeSquares) { + Bitboard occupied; + + // West from A1 (leftmost column, rank 1) - no squares available + Bitboard west_result = RookMoveGenerator::west_ray(Squares::A1, occupied); + EXPECT_EQ(west_result.count(), 0); + + // West from A8 (leftmost column, rank 8) - no squares available + west_result = RookMoveGenerator::west_ray(Squares::A8, occupied); + EXPECT_EQ(west_result.count(), 0); +} + +/** + * @test RookMoveGenerator::west_ray() from A-file with different ranks + * @brief Verifies that west_ray returns empty bitboard from any A-file square. + */ +TEST(RookMoveGeneratorTest, WestRayFromAFileDifferentRanks) { + Bitboard occupied; + + // West from A4 (leftmost column, rank 4) - no squares available + Bitboard west_result = RookMoveGenerator::west_ray(Squares::A4, occupied); + EXPECT_EQ(west_result.count(), 0); + + // West from A5 (leftmost column, rank 5) - no squares available + west_result = RookMoveGenerator::west_ray(Squares::A5, occupied); + EXPECT_EQ(west_result.count(), 0); +} + +/** + * @test RookMoveGenerator::west_ray() from center board positions + * @brief Verifies that west_ray correctly handles starting positions in the middle of the board. + */ +TEST(RookMoveGeneratorTest, WestRayFromCenterPositions) { + Bitboard occupied; + + // From D4 (middle of board) + Bitboard result = RookMoveGenerator::west_ray(Squares::D4, occupied); + EXPECT_TRUE(result.test(Square::C4)); + EXPECT_TRUE(result.test(Square::B4)); + EXPECT_TRUE(result.test(Square::A4)); + EXPECT_EQ(result.count(), 3); + + // From E5 (also middle of board) + result = RookMoveGenerator::west_ray(Squares::E5, occupied); + EXPECT_TRUE(result.test(Square::D5)); + EXPECT_TRUE(result.test(Square::C5)); + EXPECT_TRUE(result.test(Square::B5)); + EXPECT_TRUE(result.test(Square::A5)); + EXPECT_EQ(result.count(), 4); +} + +/** + * @test RookMoveGenerator::west_ray() from near-edge positions + * @brief Verifies that west_ray correctly handles starting positions near the western edge. + */ +TEST(RookMoveGeneratorTest, WestRayFromNearEdgePositions) { + Bitboard occupied; + + // From B1 (one square from edge) + Bitboard result = RookMoveGenerator::west_ray(Squares::B1, occupied); + EXPECT_TRUE(result.test(Square::A1)); + EXPECT_EQ(result.count(), 1); + + // From C3 (two squares from edge) + result = RookMoveGenerator::west_ray(Squares::C3, occupied); + EXPECT_TRUE(result.test(Square::B3)); + EXPECT_TRUE(result.test(Square::A3)); + EXPECT_EQ(result.count(), 2); +} From 6e6ae9c3ab153bd63c99144c8807ba77b09323bf Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Sun, 14 Dec 2025 16:45:54 +0100 Subject: [PATCH 4/4] test names harmonization --- .../test_rmg_add_rook_castling.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp b/tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp index bb38482..620a916 100644 --- a/tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp +++ b/tests/bitbishop/moves/rook_move_generator/test_rmg_add_rook_castling.cpp @@ -10,7 +10,7 @@ * @test RookMoveGenerator::add_rook_castling() with no castling rights * @brief Verifies that no castling moves are added when castling rights are not available. */ -TEST(RookMoveGeneratorTest, AddRookCastlingNoCastlingRights) { +TEST(RookMoveGeneratorTest, AddRookCastlingWhiteNoCastlingRights) { Board board("8/8/8/8/8/8/8/R3K2R w - - 0 1"); std::vector moves; @@ -46,7 +46,7 @@ TEST(RookMoveGeneratorTest, AddRookCastlingBlackNoCastlingRights) { * @test RookMoveGenerator::add_rook_castling() with both castling rights available * @brief Verifies that both kingside and queenside castling moves are added when both are legal. */ -TEST(RookMoveGeneratorTest, AddRookCastlingBothSidesAvailable) { +TEST(RookMoveGeneratorTest, AddRookCastlingWhiteBothSidesAvailable) { Board board("8/8/8/8/8/8/8/R3K2R w KQ - 0 1"); std::vector moves; @@ -66,7 +66,7 @@ TEST(RookMoveGeneratorTest, AddRookCastlingBothSidesAvailable) { * @test RookMoveGenerator::add_rook_castling() with only kingside castling available * @brief Verifies that only kingside castling move is added when queenside is not legal. */ -TEST(RookMoveGeneratorTest, AddRookCastlingOnlyKingsideAvailable) { +TEST(RookMoveGeneratorTest, AddRookCastlingWhiteOnlyKingsideAvailable) { Board board("8/8/8/8/8/8/8/R3K2R w K - 0 1"); std::vector moves; @@ -86,7 +86,7 @@ TEST(RookMoveGeneratorTest, AddRookCastlingOnlyKingsideAvailable) { * @test RookMoveGenerator::add_rook_castling() with only queenside castling available * @brief Verifies that only queenside castling move is added when kingside is not legal. */ -TEST(RookMoveGeneratorTest, AddRookCastlingOnlyQueensideAvailable) { +TEST(RookMoveGeneratorTest, AddRookCastlingWhiteOnlyQueensideAvailable) { Board board("8/8/8/8/8/8/8/R3K2R w Q - 0 1"); std::vector moves; @@ -166,7 +166,7 @@ TEST(RookMoveGeneratorTest, AddRookCastlingBlackOnlyQueensideAvailable) { * @test RookMoveGenerator::add_rook_castling() with pieces blocking castling * @brief Verifies that castling moves are not added when pieces block the path. */ -TEST(RookMoveGeneratorTest, AddRookCastlingBlockedByPieces) { +TEST(RookMoveGeneratorTest, AddRookCastlingWhiteBlockedByPieces) { Board board("8/8/8/8/8/8/8/R1B1KN1R w KQ - 0 1"); std::vector moves; @@ -202,7 +202,7 @@ TEST(RookMoveGeneratorTest, AddRookCastlingBlackBlockedByPieces) { * @test RookMoveGenerator::add_rook_castling() with invalid starting square for a black rook * @brief Verifies that castling moves are not added when black rook's starting position is invalid. */ -TEST(RookMoveGeneratorTest, AddRookCastlingInvalidFromSquare) { +TEST(RookMoveGeneratorTest, AddRookCastlingWhiteInvalidFromSquare) { Board board("8/8/8/8/8/8/8/R1B1KN1R w KQ - 0 1"); std::vector moves;