diff --git a/.github/workflows/linux_build_test.yaml b/.github/workflows/linux_build_test.yaml index adf52d3..4fca8b3 100644 --- a/.github/workflows/linux_build_test.yaml +++ b/.github/workflows/linux_build_test.yaml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - preset: [ clang_debug, clang_release ] + preset: [ clang_release ] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/windows_build_test.yaml b/.github/workflows/windows_build_test.yaml index b06903b..2f255fc 100644 --- a/.github/workflows/windows_build_test.yaml +++ b/.github/workflows/windows_build_test.yaml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - preset: [ msvc_debug, msvc_release ] + preset: [ msvc_release ] steps: - uses: actions/checkout@v4 diff --git a/CMakePresets.json b/CMakePresets.json index 899281b..8f2da42 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -23,7 +23,12 @@ "description": "Debug build with debug symbols and no optimizations", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_CXX_COMPILER": "clang++" + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_C_FLAGS_DEBUG": "-O0 -g -fno-inline -fno-omit-frame-pointer", + "CMAKE_CXX_FLAGS_DEBUG": "-O0 -g -fno-inline -fno-inline-functions -fno-omit-frame-pointer", + "CMAKE_EXE_LINKER_FLAGS_DEBUG": "-Wl,-force_load,${sourceDir}/build/${presetName}/src/bitbishop/libBitbishop.a", + "CMAKE_SHARED_LINKER_FLAGS_DEBUG": "-Wl,-no_dead_strip", + "CMAKE_MODULE_LINKER_FLAGS_DEBUG": "-Wl,-no_dead_strip" } }, { diff --git a/include/bitbishop/attacks/generate_attacks.hpp b/include/bitbishop/attacks/generate_attacks.hpp index 647c1e8..ebd5e6a 100644 --- a/include/bitbishop/attacks/generate_attacks.hpp +++ b/include/bitbishop/attacks/generate_attacks.hpp @@ -25,7 +25,7 @@ * @param enemy The side whose attacks are to be generated. * @return A bitboard of all squares attacked by the given side. */ -Bitboard generate_attacks(const Board& board, Color enemy) { +inline Bitboard generate_attacks(const Board& board, Color enemy) { Bitboard attacks = Bitboard::Zeros(); Bitboard occupied_no_king = board.occupied() ^ board.king(ColorUtil::opposite(enemy)); diff --git a/include/bitbishop/bitboard.hpp b/include/bitbishop/bitboard.hpp index 11fc6ff..7f34397 100644 --- a/include/bitbishop/bitboard.hpp +++ b/include/bitbishop/bitboard.hpp @@ -136,17 +136,7 @@ class Bitboard { * * The output starts from rank 8 down to rank 1. */ - void print() const { - using namespace Const; - - for (int rank = RANK_8_IND; rank >= RANK_1_IND; --rank) { - for (int file = FILE_A_IND; file <= FILE_H_IND; ++file) { - const Square square(file, rank); - std::cout << (test(square) ? "1 " : ". "); - } - std::cout << "\n"; - } - } + void print() const; constexpr bool operator==(const Bitboard& other) const { return m_bb == other.m_bb; } constexpr bool operator!=(const Bitboard& other) const { return m_bb != other.m_bb; } diff --git a/include/bitbishop/move.hpp b/include/bitbishop/move.hpp index 1832b6a..68aa927 100644 --- a/include/bitbishop/move.hpp +++ b/include/bitbishop/move.hpp @@ -21,6 +21,42 @@ struct Move { bool is_en_passant; ///< True if the move is an en passant capture. bool is_castling; ///< True if the move is a castling move (kingside or queenside). + /** + * @brief Converts move to UCI notation. + * @return String in UCI format (e.g., "e2e4", "e7e8q") + */ + [[nodiscard]] std::string to_uci() const { + static constexpr std::size_t MAX_NB_CHARS_IN_UCI_MOVE_REPR = 5; + + std::string uci; + uci.reserve(MAX_NB_CHARS_IN_UCI_MOVE_REPR); + + uci += from.to_string(); + uci += to.to_string(); + + using namespace Squares; + if (promotion) { + switch (promotion.value().type()) { + case Piece::Type::QUEEN: + uci += 'q'; + break; + case Piece::Type::ROOK: + uci += 'r'; + break; + case Piece::Type::BISHOP: + uci += 'b'; + break; + case Piece::Type::KNIGHT: + uci += 'n'; + break; + default: + break; + } + } + + return uci; + } + /** * @brief Creates a normal (non-special) move. * @param from The starting square of the move. diff --git a/include/bitbishop/movegen/bishop_moves.hpp b/include/bitbishop/movegen/bishop_moves.hpp index c24f3c5..830c2ac 100644 --- a/include/bitbishop/movegen/bishop_moves.hpp +++ b/include/bitbishop/movegen/bishop_moves.hpp @@ -44,8 +44,8 @@ * - This function assumes that @p check_mask and @p pins have already been * computed for the current position. */ -void generate_bishop_legal_moves(std::vector& moves, const Board& board, Color us, const Bitboard& check_mask, - const PinResult& pins) { +inline void generate_bishop_legal_moves(std::vector& moves, const Board& board, Color us, + const Bitboard& check_mask, const PinResult& pins) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); const Bitboard occupied = board.occupied(); diff --git a/include/bitbishop/movegen/castling_moves.hpp b/include/bitbishop/movegen/castling_moves.hpp index 58afc48..e5c79a4 100644 --- a/include/bitbishop/movegen/castling_moves.hpp +++ b/include/bitbishop/movegen/castling_moves.hpp @@ -27,8 +27,8 @@ * @param checkers Bitboard of pieces currently checking the king * @param enemy_attacks Bitboard of squares attacked by the opponent */ -void generate_castling_moves(std::vector& moves, const Board& board, Color us, const Bitboard& checkers, - const Bitboard& enemy_attacks) { +inline void generate_castling_moves(std::vector& moves, const Board& board, Color us, const Bitboard& checkers, + const Bitboard& enemy_attacks) { using namespace Squares; if (checkers.any()) { diff --git a/include/bitbishop/movegen/king_moves.hpp b/include/bitbishop/movegen/king_moves.hpp index 4098a1e..2edafdb 100644 --- a/include/bitbishop/movegen/king_moves.hpp +++ b/include/bitbishop/movegen/king_moves.hpp @@ -7,8 +7,8 @@ #include #include -void generate_legal_king_moves(std::vector& moves, const Board& board, Color us, Square king_sq, - const Bitboard& enemy_attacks) { +inline void generate_legal_king_moves(std::vector& moves, const Board& board, Color us, Square king_sq, + const Bitboard& enemy_attacks) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); diff --git a/include/bitbishop/movegen/knight_moves.hpp b/include/bitbishop/movegen/knight_moves.hpp index 50f1297..b1d2c02 100644 --- a/include/bitbishop/movegen/knight_moves.hpp +++ b/include/bitbishop/movegen/knight_moves.hpp @@ -9,8 +9,8 @@ #include // pinned knights cannot move at all due to knight's l-shaped move geometry -void generate_knight_legal_moves(std::vector& moves, const Board& board, Color us, const Bitboard& check_mask, - const PinResult& pins) { +inline void generate_knight_legal_moves(std::vector& moves, const Board& board, Color us, + const Bitboard& check_mask, const PinResult& pins) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); Bitboard knights = board.knights(us); diff --git a/include/bitbishop/movegen/legal_moves.hpp b/include/bitbishop/movegen/legal_moves.hpp index e9eaf0d..f5aabe0 100644 --- a/include/bitbishop/movegen/legal_moves.hpp +++ b/include/bitbishop/movegen/legal_moves.hpp @@ -36,15 +36,16 @@ * * @param moves Vector to append generated legal moves to * @param board Current board position - * @param us Color of the side to generate moves for * * @note The move list is appended to; it is not cleared by this function. * @note Assumes the board position is internally consistent and legal. */ -void generate_legal_moves(std::vector& moves, const Board& board, Color us) { - Square king_sq = board.king_square(us).value(); +inline void generate_legal_moves(std::vector& moves, const Board& board) { + Color us = board.get_state().m_is_white_turn ? Color::WHITE : Color::BLACK; Color them = ColorUtil::opposite(us); + Square king_sq = board.king_square(us).value(); + Bitboard checkers = compute_checkers(board, king_sq, them); Bitboard check_mask = compute_check_mask(king_sq, checkers, board); PinResult pins = compute_pins(king_sq, board, us); diff --git a/include/bitbishop/movegen/pawn_moves.hpp b/include/bitbishop/movegen/pawn_moves.hpp index aee07ae..89f04a1 100644 --- a/include/bitbishop/movegen/pawn_moves.hpp +++ b/include/bitbishop/movegen/pawn_moves.hpp @@ -94,7 +94,7 @@ constexpr bool can_capture_en_passant(Square from, Square epsq, Color side) noex * @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) { +inline 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) { @@ -224,14 +224,6 @@ inline void generate_en_passant(std::vector& moves, Square from, Color us, 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); @@ -260,8 +252,8 @@ inline void generate_en_passant(std::vector& moves, Square from, Color us, * @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) { +inline 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); diff --git a/include/bitbishop/movegen/queen_moves.hpp b/include/bitbishop/movegen/queen_moves.hpp index 790683e..f1389c2 100644 --- a/include/bitbishop/movegen/queen_moves.hpp +++ b/include/bitbishop/movegen/queen_moves.hpp @@ -45,8 +45,8 @@ * computed for the current position. * - Promotions, en passant, and castling are not applicable to queen moves. */ -void generate_queen_legal_moves(std::vector& moves, const Board& board, Color us, const Bitboard& check_mask, - const PinResult& pins) { +inline void generate_queen_legal_moves(std::vector& moves, const Board& board, Color us, + const Bitboard& check_mask, const PinResult& pins) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); const Bitboard occupied = board.occupied(); diff --git a/include/bitbishop/movegen/rook_moves.hpp b/include/bitbishop/movegen/rook_moves.hpp index 533f01b..9b0c72c 100644 --- a/include/bitbishop/movegen/rook_moves.hpp +++ b/include/bitbishop/movegen/rook_moves.hpp @@ -45,8 +45,8 @@ * computed for the current position. * - Promotions, en passant, and castling are not applicable to rook moves. */ -void generate_rook_legal_moves(std::vector& moves, const Board& board, Color us, const Bitboard& check_mask, - const PinResult& pins) { +inline void generate_rook_legal_moves(std::vector& moves, const Board& board, Color us, + const Bitboard& check_mask, const PinResult& pins) { const Bitboard own = board.friendly(us); const Bitboard enemy = board.enemy(us); const Bitboard occupied = board.occupied(); diff --git a/include/bitbishop/moves/position.hpp b/include/bitbishop/moves/position.hpp index 3029a67..43437c4 100644 --- a/include/bitbishop/moves/position.hpp +++ b/include/bitbishop/moves/position.hpp @@ -5,94 +5,44 @@ #include /** - * @brief Represents a game position, including board state and move history. + * @brief Represents a chess position and move history. * - * A Position owns a Board instance and provides the ability to apply and revert moves. - * It is primarily responsible for maintaining a consistent board state across move - * generation, execution, and undo operations. - * - * Key responsibilities: - * - Track the current board state - * - Execute moves and record them for later rollback - * - Revert moves safely via the stored execution history - * - * The class is intentionally non-copyable and non-default-constructible to avoid - * ambiguous or partially-initialized board states. A fully-initialized Board must be - * provided at construction time. + * Tracks a Board (by reference) and allows applying/reverting moves. The Position + * itself does not own the Board; it modifies the provided board safely using + * MoveExecution history. */ class Position { private: - /** - * @brief The current board state of the position. - * - * This board is updated every time a move is applied or reverted. The Position - * class provides controlled access to this board to ensure move history and - * board state remain synchronized. - */ - Board board; + /** Reference to the board being managed */ + Board& board; - /** - * @brief History of executed moves, including auxiliary information. - * - * Each entry contains the data necessary to revert a move accurately, including - * captured pieces, castling rights changes, en passant information, etc. - * - * Moves are pushed when applied and popped when reverted. - */ + /** History of executed moves for rollback */ std::vector move_execution_history; public: - Position() = delete; ///< Must be initialized with a Board - Position(Board&) = delete; ///< Prevent accidental copy from lvalue Board - - /** - * @brief Constructs a Position from an initial board state. - * - * Although this is declared as a move constructor, Board is trivially copyable, - * so the actual internal assignment is effectively a copy. This constructor - * ensures that the Position starts with a valid, fully defined board. - * - * @param initial The initial board state (rvalue reference) - */ - explicit Position(Board&& initial) : board(initial) { - ; // board is trivially copyable, no move, just a copy behind the scenes - } + Position() = delete; ///< Default construction not allowed + Position(Board& board) : board(board) { ; } /** - * @brief Applies a move to the current position. - * - * This method: - * - Computes the full execution details of the move - * - Updates the board state accordingly - * - Stores the MoveExecution record so the move can be reverted later - * - * @param move The move to apply + * @brief Applies a move to the board and records it for undo. + * @param move Move to apply */ void apply_move(const Move& move); /** * @brief Reverts the last applied move. - * - * Pops the most recent entry from the move execution history and restores the - * board to its previous state. Calling this function when no moves have been - * applied have no effect.. + * Safe to call only if can_unmake() returns true. */ void revert_move(); /** - * @brief Returns a const reference to the current board. - * - * Provides read-only access to the board; callers must not attempt to mutate - * the board directly to avoid desynchronizing board state and move history. - * - * @return Const reference to the current Board + * @brief Returns the current board (read-only). */ [[nodiscard]] const Board& get_board() const { return board; } /** - * @brief Checks whether a previously applied move can be reverted. - * - * @return true if at least one move has been applied, false otherwise + * @brief Checks if a move can be reverted. + * @return true if move history is non-empty */ [[nodiscard]] bool can_unmake() const { return !move_execution_history.empty(); } }; diff --git a/include/bitbishop/tools/perft.hpp b/include/bitbishop/tools/perft.hpp new file mode 100644 index 0000000..55c91c2 --- /dev/null +++ b/include/bitbishop/tools/perft.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace Tools { + +/** + * @brief Perft (Performance Test) debug function to walk through the move generation tree + * of legal moves of a certain depth. + * + * @param board Reference to the board on which perft must be executed + * @param depth Recursion depth + * + * @see https://www.chessprogramming.org/Perft + */ +uint64_t perft(Board& board, std::size_t depth); + +/** + * @brief Perft Divide (Performance Test) debug function to walk through the move generation tree + * of legal moves of a certain depth and print move count for each move. + * + * @param board Reference to the board on which perft divide must be executed + * @param depth Recursion depth + * + * @see https://www.chessprogramming.org/Perft + */ +void perft_divide(Board& board, std::size_t depth); + +} // namespace Tools diff --git a/src/bitbishop/bitboard.cpp b/src/bitbishop/bitboard.cpp new file mode 100644 index 0000000..619bd66 --- /dev/null +++ b/src/bitbishop/bitboard.cpp @@ -0,0 +1,15 @@ +#include + +void Bitboard::print() const { + // defined in cpp to allow usage inside debugger + + using namespace Const; + + for (int rank = RANK_8_IND; rank >= RANK_1_IND; --rank) { + for (int file = FILE_A_IND; file <= FILE_H_IND; ++file) { + const Square square(file, rank); + std::cout << (test(square) ? "1 " : ". "); + } + std::cout << "\n"; + } +} diff --git a/src/bitbishop/tools/perft.cpp b/src/bitbishop/tools/perft.cpp new file mode 100644 index 0000000..32a91de --- /dev/null +++ b/src/bitbishop/tools/perft.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +uint64_t Tools::perft(Board& board, std::size_t depth) { + uint64_t nodes = 0; + + if (depth == 0) { + return 1; + } + + std::vector moves; + generate_legal_moves(moves, board); + + Position position(board); + for (const Move& move : moves) { + position.apply_move(move); + nodes += perft(board, depth - 1); + position.revert_move(); + } + return nodes; +} + +void Tools::perft_divide(Board& board, std::size_t depth) { + uint64_t total_nodes = 0; + + std::vector moves; + generate_legal_moves(moves, board); + + Position position(board); + for (const Move& move : moves) { + position.apply_move(move); + uint64_t nodes = (depth == 1) ? 1 : perft(board, depth - 1); + position.revert_move(); + + std::cout << move.to_uci() << ": " << nodes << "\n"; + total_nodes += nodes; + } + + std::cout << "\nNodes searched: " << total_nodes << "\n"; +} diff --git a/tests/bitbishop/movegen/test_legal_moves.cpp b/tests/bitbishop/movegen/test_legal_moves.cpp index 4d5b5cd..9b4a669 100644 --- a/tests/bitbishop/movegen/test_legal_moves.cpp +++ b/tests/bitbishop/movegen/test_legal_moves.cpp @@ -17,9 +17,12 @@ using namespace Pieces; */ TEST(GenerateLegalMovesTest, StartingPositionWhite) { Board board = Board::StartingPosition(); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // 16 pawn moves (8 single + 8 double) // 4 knight moves (2 knights * 2 each) @@ -33,9 +36,12 @@ TEST(GenerateLegalMovesTest, StartingPositionWhite) { */ TEST(GenerateLegalMovesTest, StartingPositionBlack) { Board board = Board::StartingPosition(); + BoardState state = board.get_state(); + state.m_is_white_turn = false; + board.set_state(state); std::vector moves; - generate_legal_moves(moves, board, Color::BLACK); + generate_legal_moves(moves, board); // 16 pawn moves + 4 knight moves EXPECT_EQ(moves.size(), 20); @@ -51,8 +57,12 @@ TEST(GenerateLegalMovesTest, OnlyKingsOnBoard) { board.set_piece(E1, WHITE_KING); board.set_piece(E8, BLACK_KING); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // King has 5 moves (on edge, some squares attacked by black king) EXPECT_GT(moves.size(), 0); @@ -66,9 +76,12 @@ TEST(GenerateLegalMovesTest, OnlyKingsOnBoard) { */ TEST(GenerateLegalMovesTest, KingInSingleCheck) { Board board("rnb1kbnr/pppp1ppp/8/4p3/6Pq/3P1P2/PPP1P2P/RNBQKBNR b KQkq - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); EXPECT_GT(moves.size(), 0); } @@ -80,9 +93,12 @@ TEST(GenerateLegalMovesTest, KingInSingleCheck) { */ TEST(GenerateLegalMovesTest, KingInDoubleCheck) { Board board("4k3/8/8/8/8/3r4/3r4/4K3 w - - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Only king moves allowed in double check for (const Move& move : moves) { @@ -101,8 +117,12 @@ TEST(GenerateLegalMovesTest, PinnedPiecesCanMove) { board.set_piece(E4, WHITE_ROOK); board.set_piece(E8, BLACK_ROOK); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Pinned rook can move along pin ray EXPECT_TRUE(contains_move(moves, {E4, E5, std::nullopt, false, false, false})); @@ -120,8 +140,12 @@ TEST(GenerateLegalMovesTest, PinnedPiecesCanMove) { TEST(GenerateLegalMovesTest, CastlingIncluded) { Board board("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); EXPECT_TRUE(contains_move(moves, {E1, G1, std::nullopt, false, false, true})); EXPECT_TRUE(contains_move(moves, {E1, C1, std::nullopt, false, false, true})); @@ -135,8 +159,12 @@ TEST(GenerateLegalMovesTest, CastlingIncluded) { TEST(GenerateLegalMovesTest, NoCastlingWhenInCheck) { Board board("r3k2r/8/8/8/8/8/4q3/R3K2R w KQkq - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); EXPECT_FALSE(contains_move(moves, {E1, G1, std::nullopt, false, false, true})); EXPECT_FALSE(contains_move(moves, {E1, C1, std::nullopt, false, false, true})); @@ -156,8 +184,12 @@ TEST(GenerateLegalMovesTest, AllPieceTypesGenerate) { board.set_piece(B1, WHITE_QUEEN); board.set_piece(E8, BLACK_KING); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Should have moves from all piece types bool has_king_move = false; @@ -194,8 +226,12 @@ TEST(GenerateLegalMovesTest, PawnPromotionsIncluded) { board.set_piece(E7, WHITE_PAWN); board.set_piece(A8, BLACK_KING); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Should have 4 promotion moves EXPECT_TRUE(contains_move(moves, {E7, E8, WHITE_QUEEN, false, false, false})); @@ -211,8 +247,12 @@ TEST(GenerateLegalMovesTest, PawnPromotionsIncluded) { TEST(GenerateLegalMovesTest, EnPassantIncluded) { Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); } @@ -228,8 +268,12 @@ TEST(GenerateLegalMovesTest, CapturesIncluded) { board.set_piece(E7, BLACK_PAWN); board.set_piece(E8, BLACK_KING); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); EXPECT_TRUE(contains_move(moves, {E4, E7, std::nullopt, true, false, false})); } @@ -245,8 +289,12 @@ TEST(GenerateLegalMovesTest, NoIllegalMoves) { board.set_piece(E2, WHITE_QUEEN); board.set_piece(E8, BLACK_ROOK); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // White queen cannot move east or west as it would expose the king EXPECT_FALSE(contains_move(moves, {E2, D2, std::nullopt, false, false, false})); @@ -270,11 +318,15 @@ TEST(GenerateLegalMovesTest, MovesVectorNotCleared) { board.set_piece(E1, WHITE_KING); board.set_piece(E8, BLACK_KING); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; moves.emplace_back(Move::make(A1, A2)); size_t initial_size = moves.size(); - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); EXPECT_GT(moves.size(), initial_size); EXPECT_TRUE(contains_move(moves, Move::make(A1, A2))); @@ -287,8 +339,12 @@ TEST(GenerateLegalMovesTest, MovesVectorNotCleared) { TEST(GenerateLegalMovesTest, StalemateNoMoves) { Board board("7k/5Q2/6K1/8/8/8/8/8 b - - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = false; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::BLACK); + generate_legal_moves(moves, board); EXPECT_EQ(moves.size(), 0); } @@ -300,8 +356,12 @@ TEST(GenerateLegalMovesTest, StalemateNoMoves) { TEST(GenerateLegalMovesTest, CheckmatePosition) { Board board("8/8/8/8/8/8/1r6/r2K4 w - - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // King in checkmate - no legal moves EXPECT_EQ(moves.size(), 0); @@ -315,8 +375,12 @@ TEST(GenerateLegalMovesTest, CheckmatePosition) { TEST(GenerateLegalMovesTest, BackRankMateThreat) { Board board("6k1/5ppp/8/8/8/8/5PPP/5RK1 w - - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Should generate moves but not illegal king moves EXPECT_GT(moves.size(), 0); @@ -333,8 +397,12 @@ TEST(GenerateLegalMovesTest, DiscoveredCheckRestriction) { board.set_piece(E4, WHITE_BISHOP); board.set_piece(E8, BLACK_ROOK); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Bishop pinned, cannot move for (const Move& move : moves) { @@ -349,8 +417,12 @@ TEST(GenerateLegalMovesTest, DiscoveredCheckRestriction) { TEST(GenerateLegalMovesTest, ComplexPosition) { Board board("r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Should have many legal moves EXPECT_GT(moves.size(), 20); @@ -364,8 +436,12 @@ TEST(GenerateLegalMovesTest, ComplexPosition) { TEST(GenerateLegalMovesTest, OnlyKingMovesInDoubleCheck) { Board board("4k3/8/8/8/8/2q5/2r5/4K3 w - - 0 1"); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // All moves should be from the king for (const Move& move : moves) { @@ -383,8 +459,12 @@ TEST(GenerateLegalMovesTest, BlockingMovesIncluded) { board.set_piece(D2, WHITE_BISHOP); board.set_piece(E8, BLACK_ROOK); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Bishop can block on E file bool has_blocking_move = false; @@ -407,8 +487,12 @@ TEST(GenerateLegalMovesTest, CapturingCheckerIncluded) { board.set_piece(D2, WHITE_KNIGHT); board.set_piece(E5, BLACK_QUEEN); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Should include capturing the queen (if knight can reach it) // Or king moving away @@ -422,8 +506,12 @@ TEST(GenerateLegalMovesTest, CapturingCheckerIncluded) { TEST(GenerateLegalMovesTest, NoDuplicateMoves) { Board board = Board::StartingPosition(); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Check for duplicates for (size_t i = 0; i < moves.size(); i++) { @@ -446,8 +534,12 @@ TEST(GenerateLegalMovesTest, KnightMovesIncluded) { board.set_piece(D4, WHITE_KNIGHT); board.set_piece(E8, BLACK_KING); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Knight should have moves bool has_knight_move = false; @@ -470,8 +562,12 @@ TEST(GenerateLegalMovesTest, PinnedKnightNoMoves) { board.set_piece(E3, WHITE_KNIGHT); board.set_piece(E8, BLACK_ROOK); + BoardState state = board.get_state(); + state.m_is_white_turn = true; + board.set_state(state); + std::vector moves; - generate_legal_moves(moves, board, Color::WHITE); + generate_legal_moves(moves, board); // Pinned knight cannot move for (const Move& move : moves) { 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 index 42e656a..f41cf00 100644 --- 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 @@ -627,11 +627,11 @@ TEST(GeneratePawnLegalMovesTest, NoEnPassantWithoutTarget) { } /** - * @test En passant blocked by check mask. - * @brief Confirms generate_pawn_legal_moves() does not generate en passant + * @test En passant is not blocked by check mask. + * @brief Confirms generate_pawn_legal_moves() does generate en passant * when target square not in check mask. */ -TEST(GeneratePawnLegalMovesTest, EnPassantBlockedByCheckMask) { +TEST(GeneratePawnLegalMovesTest, EnPassantAllowedByCheckMask) { Board board("rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1"); Bitboard check_mask = Bitboard::Zeros(); @@ -642,8 +642,8 @@ TEST(GeneratePawnLegalMovesTest, EnPassantBlockedByCheckMask) { 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})); + // En passant still allowed + EXPECT_TRUE(contains_move(moves, {D5, E6, std::nullopt, true, true, false})); } /** diff --git a/tests/bitbishop/moves/test_position.cpp b/tests/bitbishop/moves/test_position.cpp index 2d506d6..8ccb20e 100644 --- a/tests/bitbishop/moves/test_position.cpp +++ b/tests/bitbishop/moves/test_position.cpp @@ -12,7 +12,7 @@ TEST(PositionTest, ApplyMoveUpdatesBoard) { Board board = Board::Empty(); board.set_piece(E2, WHITE_PAWN); - Position pos(std::move(std::move(board))); + Position pos(board); Move move = Move::make(E2, E4, false); @@ -29,7 +29,7 @@ TEST(PositionTest, RevertMoveRestoresBoard) { Board board = Board::Empty(); board.set_piece(E2, WHITE_PAWN); - Position pos(std::move(board)); + Position pos(board); Move move = Move::make(E2, E4, false); @@ -50,7 +50,7 @@ TEST(PositionTest, CanUnmakeReflectsMoveHistory) { Board board = Board::Empty(); board.set_piece(E2, WHITE_PAWN); - Position pos(std::move(board)); + Position pos(board); // No moves yet EXPECT_FALSE(pos.can_unmake()); diff --git a/tests/bitbishop/tools/test_perft.cpp b/tests/bitbishop/tools/test_perft.cpp new file mode 100644 index 0000000..14fea0a --- /dev/null +++ b/tests/bitbishop/tools/test_perft.cpp @@ -0,0 +1,266 @@ +#include + +#include +#include + +struct PerftTestCase { + std::string test_name; + std::string fen; + std::size_t depth; + uint64_t expected_nodes_count; +}; + +struct PerftParamName { + template + std::string operator()(const testing::TestParamInfo& info) const { + return info.param.test_name; + } +}; + +class PerftTest : public ::testing::TestWithParam {}; + +TEST_P(PerftTest, PerftMatchesExpected) { + const auto& param = GetParam(); + + Board board(param.fen); + uint64_t nodes = Tools::perft(board, param.depth); + + EXPECT_EQ(nodes, param.expected_nodes_count) << "FEN: " << param.fen << "\nDepth: " << param.depth; +} + +// Test cases from: https://www.chessprogramming.org/Perft_Results +static constexpr const char* STARTING_POS = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +static constexpr const char* KIWIPETE_POS = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; +static constexpr const char* POSITION_THREE = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1"; +static constexpr const char* POSITION_FOUR = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"; +static constexpr const char* POSITION_FIVE = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"; +static constexpr const char* POSITION_SIX = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"; + +// clang-format off +INSTANTIATE_TEST_SUITE_P( + PerftValidation, + PerftTest, + ::testing::Values( + // Starting position: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 + PerftTestCase{ + "StartingPos_Depth0", + STARTING_POS, 0, 1 + }, + PerftTestCase{ + "StartingPos_Depth1", + STARTING_POS, 1, 20 + }, + PerftTestCase{ + "StartingPos_Depth2", + STARTING_POS, 2, 400 + }, + PerftTestCase{ + "StartingPos_Depth3", + STARTING_POS, 3, 8'902 + }, + PerftTestCase{ + "StartingPos_Depth4", + STARTING_POS, 4, 197'281 + }, + PerftTestCase{ + "StartingPos_Depth5", + STARTING_POS, 5, 4'865'609 + }, + PerftTestCase{ + "StartingPos_Depth6", + STARTING_POS, 6, 119'060'324 + }, + + // Kiwipete position: r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - + PerftTestCase{ + "KiwipetePos_Depth0", + KIWIPETE_POS, 0, 1 + }, + PerftTestCase{ + "KiwipetePos_Depth1", + KIWIPETE_POS, 1, 48 + }, + PerftTestCase{ + "KiwipetePos_Depth2", + KIWIPETE_POS, 2, 2'039 + }, + PerftTestCase{ + "KiwipetePos_Depth3", + KIWIPETE_POS, 3, 97'862 + }, + PerftTestCase{ + "KiwipetePos_Depth4", + KIWIPETE_POS, 4, 4'085'603 + }, + PerftTestCase{ + "KiwipetePos_Depth5", + KIWIPETE_POS, 5, 193'690'690 + }, + PerftTestCase{ + "KiwipetePos_Depth6", + KIWIPETE_POS, 6, 8'031'647'685 + }, + + // Position 3: 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1 + PerftTestCase{ + "Position3_Depth0", + POSITION_THREE, 0, 1 + }, + PerftTestCase{ + "Position3_Depth1", + POSITION_THREE, 1, 14 + }, + PerftTestCase{ + "Position3_Depth2", + POSITION_THREE, 2, 191 + }, + PerftTestCase{ + "Position3_Depth3", + POSITION_THREE, 3, 2'812 + }, + PerftTestCase{ + "Position3_Depth4", + POSITION_THREE, 4, 43'238 + }, + PerftTestCase{ + "Position3_Depth5", + POSITION_THREE, 5, 674'624 + }, + PerftTestCase{ + "Position3_Depth6", + POSITION_THREE, 6, 11'030'083 + }, + + // Position 4: r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1 + PerftTestCase{ + "Position4_Depth0", + POSITION_FOUR, 0, 1 + }, + PerftTestCase{ + "Position4_Depth1", + POSITION_FOUR, 1, 6 + }, + PerftTestCase{ + "Position4_Depth2", + POSITION_FOUR, 2, 264 + }, + PerftTestCase{ + "Position4_Depth3", + POSITION_FOUR, 3, 9'467 + }, + PerftTestCase{ + "Position4_Depth4", + POSITION_FOUR, 4, 422'333 + }, + PerftTestCase{ + "Position4_Depth5", + POSITION_FOUR, 5, 15'833'292 + }, + PerftTestCase{ + "Position4_Depth6", + POSITION_FOUR, 6, 706'045'033 + }, + + // Position 5: rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8 + PerftTestCase{ + "Position5_Depth0", + POSITION_FIVE, 0, 1 + }, + PerftTestCase{ + "Position5_Depth1", + POSITION_FIVE, 1, 44 + }, + PerftTestCase{ + "Position5_Depth2", + POSITION_FIVE, 2, 1'486 + }, + PerftTestCase{ + "Position5_Depth3", + POSITION_FIVE, 3, 62'379 + }, + PerftTestCase{ + "Position5_Depth4", + POSITION_FIVE, 4, 2'103'487 + }, + PerftTestCase{ + "Position5_Depth5", + POSITION_FIVE, 5, 89'941'194 + }, + PerftTestCase{ + "Position5_Depth6", + POSITION_FIVE, 6, 3'048'196'529 + }, + + // Position 6: r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10 + PerftTestCase{ + "Position6_Depth0", + POSITION_SIX, 0, 1 + }, + PerftTestCase{ + "Position6_Depth1", + POSITION_SIX, 1, 46 + }, + PerftTestCase{ + "Position6_Depth2", + POSITION_SIX, 2, 2'079 + }, + PerftTestCase{ + "Position6_Depth3", + POSITION_SIX, 3, 89'890 + }, + PerftTestCase{ + "Position6_Depth4", + POSITION_SIX, 4, 3'894'594 + }, + PerftTestCase{ + "Position6_Depth5", + POSITION_SIX, 5, 164'075'551 + }, + PerftTestCase{ + "Position6_Depth6", + POSITION_SIX, 6, 6'923'051'137 + }, + + // Custom and specialized positions + PerftTestCase{ + "OnlyKings_Depth1", + "4k3/8/8/8/8/8/8/4K3 w - - 0 1", 1, 5 + }, + PerftTestCase{ + "SinglePawn_Depth1", + "4k3/8/8/8/8/8/4P3/4K3 w - - 0 1", 1, 6 + }, + PerftTestCase{ + "PawnPromotion_Depth1", + "4k3/4P3/8/8/8/8/8/4K3 w - - 0 1", 1, 5 + }, + PerftTestCase{ + "Stalemate_Depth1", + "7k/5Q2/6K1/8/8/8/8/8 b - - 0 1", 1, 0 + }, + PerftTestCase{ + "Checkmate_Depth1", + "6rk/6pp/7r/8/8/8/8/4K3 w - - 0 1", 1, 5 + }, + PerftTestCase{ + "EnPassantAvailable_Depth1", + "rnbqkbnr/pppp1ppp/8/3Pp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 1", 1, 30 + } + ), + PerftParamName() +); +// clang-format on + +/** + * @test Perft symmetry between white and black. + * @brief Confirms perft produces same count from symmetric positions. + */ +TEST(PerftTest, SymmetricPositionsEqual) { + Board white_to_move = Board::StartingPosition(); + Board black_to_move("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1"); + + uint64_t white_nodes = Tools::perft(white_to_move, 1); + uint64_t black_nodes = Tools::perft(black_to_move, 1); + + EXPECT_EQ(white_nodes, black_nodes); +}