diff --git a/.clang-tidy b/.clang-tidy index a01d922..479eefd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -13,4 +13,4 @@ CheckOptions: - key: readability-identifier-length.IgnoredParameterNames value: "^(sq|to|from|bb|us)$" - key: readability-identifier-length.IgnoredLoopCounterNames - value: "^(r|f)$" + value: "^(r|f|i)$" diff --git a/include/bitbishop/bitboard.hpp b/include/bitbishop/bitboard.hpp index 99da1cb..11fc6ff 100644 --- a/include/bitbishop/bitboard.hpp +++ b/include/bitbishop/bitboard.hpp @@ -48,7 +48,10 @@ class Bitboard { constexpr Bitboard(uint64_t value) : m_bb(value) {} /** @brief Constructs a bitboard from another bitboard by copy. */ - constexpr Bitboard(const Bitboard& bitboard) : m_bb(bitboard.value()) {} + constexpr Bitboard(const Bitboard& bitboard) noexcept = default; + + /** @brief Move-constructs a bitboard. */ + constexpr explicit Bitboard(Bitboard&& other) noexcept = default; /** @brief Constructs a bitboard with the given square being the only bit set to one. */ constexpr Bitboard(Square square) : m_bb(0ULL) { set(square); } @@ -166,6 +169,8 @@ class Bitboard { return *this; } constexpr operator bool() const noexcept { return m_bb != 0ULL; } + constexpr Bitboard& operator=(const Bitboard& other) noexcept = default; + constexpr Bitboard& operator=(Bitboard&& other) noexcept = default; /** * @brief Counts the number of set bits in the bitboard. diff --git a/include/bitbishop/board.hpp b/include/bitbishop/board.hpp index c10795e..d2ea5e6 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -2,9 +2,42 @@ #include #include +#include #include #include #include +#include + +struct BoardState { + bool m_is_white_turn; ///< True if it is White's turn + std::optional m_en_passant_sq; ///< En passant target square, or nullopt if none + + // Castling abilities + bool m_white_castle_kingside; ///< White may castle kingside + bool m_white_castle_queenside; ///< White may castle queenside + bool m_black_castle_kingside; ///< Black may castle kingside + bool m_black_castle_queenside; ///< Black may castle queenside + + // 50-move rule state + int m_halfmove_clock; ///< Counts halfmoves since last pawn move or capture + + // Move number (starts at 1, incremented after Black’s move) + int m_fullmove_number; + + bool operator==(const BoardState& other) const { + if (this == &other) { + return true; + } + return m_is_white_turn == other.m_is_white_turn && m_en_passant_sq == other.m_en_passant_sq && + m_white_castle_kingside == other.m_white_castle_kingside && + m_white_castle_queenside == other.m_white_castle_queenside && + m_black_castle_kingside == other.m_black_castle_kingside && + m_black_castle_queenside == other.m_black_castle_queenside && m_halfmove_clock == other.m_halfmove_clock && + m_fullmove_number == other.m_fullmove_number; + } + + bool operator!=(const BoardState& other) const { return !(*this == other); } +}; /** * @class Board @@ -31,20 +64,7 @@ class Board { Bitboard m_b_pawns, m_b_rooks, m_b_bishops, m_b_knights, m_b_king, m_b_queens; // Game state - bool m_is_white_turn; ///< True if it is White's turn - std::optional m_en_passant_sq; ///< En passant target square, or nullopt if none - - // Castling abilities - bool m_white_castle_kingside; ///< White may castle kingside - bool m_white_castle_queenside; ///< White may castle queenside - bool m_black_castle_kingside; ///< Black may castle kingside - bool m_black_castle_queenside; ///< Black may castle queenside - - // 50-move rule state - int m_halfmove_clock; ///< Counts halfmoves since last pawn move or capture - - // Move number (starts at 1, incremented after Black’s move) - int m_fullmove_number; + BoardState m_state; public: /** @@ -56,7 +76,8 @@ class Board { */ Board(); - Board(const Board&) = default; + Board(const Board&) noexcept = default; + explicit Board(Board&& other) noexcept = default; /** * @brief Constructs a board from a FEN string. @@ -221,6 +242,9 @@ class Board { */ [[nodiscard]] Bitboard friendly(Color side) const { return (side == Color::WHITE) ? white_pieces() : black_pieces(); } + [[nodiscard]] BoardState get_state() const { return m_state; } + void set_state(BoardState state) { m_state = state; } + /** * @brief Returns the current en passant target square, if any. * @@ -234,7 +258,7 @@ class Board { * } * @endcode */ - [[nodiscard]] std::optional en_passant_square() const noexcept { return m_en_passant_sq; } + [[nodiscard]] std::optional en_passant_square() const noexcept { return m_state.m_en_passant_sq; } /** * @brief Checks if the given side has kingside castling rights. @@ -242,7 +266,7 @@ class Board { * @return true if kingside castling rights is available, false otherwise */ [[nodiscard]] bool has_kingside_castling_rights(Color side) const { - return (side == Color::WHITE) ? m_white_castle_kingside : m_black_castle_kingside; + return (side == Color::WHITE) ? m_state.m_white_castle_kingside : m_state.m_black_castle_kingside; } /** @@ -251,7 +275,7 @@ class Board { * @return true if queenside castling rights is available, false otherwise */ [[nodiscard]] bool has_queenside_castling_rights(Color side) const { - return (side == Color::WHITE) ? m_white_castle_queenside : m_black_castle_queenside; + return (side == Color::WHITE) ? m_state.m_white_castle_queenside : m_state.m_black_castle_queenside; } /** @@ -282,7 +306,8 @@ class Board { */ [[nodiscard]] bool can_castle_queenside(Color side) const noexcept; - Board& operator=(const Board& other) = default; + Board& operator=(const Board& other) noexcept = default; + Board& operator=(Board&& other) noexcept = default; /** * @brief Checks if two boards represent the same chess position. diff --git a/include/bitbishop/moves/move_builder.hpp b/include/bitbishop/moves/move_builder.hpp new file mode 100644 index 0000000..83b45f5 --- /dev/null +++ b/include/bitbishop/moves/move_builder.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +/** + * @brief Constructs a sequence of low-level effects for a move. + * + * MoveBuilder takes a high-level Move and a Board, and generates a + * MoveExecution that includes piece placements, removals, promotions, + * castling, en passant, and board state updates. + */ +class MoveBuilder { + private: + MoveExecution effects; ///< Stores the sequence of effects for the move + + const Move& move; ///< Reference to the move being built + const Board& board; ///< Reference to the board on which the move occurs + + Piece final_piece = Pieces::WHITE_KING; ///< Piece to place at destination + Piece moving_piece = Pieces::WHITE_KING; ///< Piece moving from origin + std::optional opt_captured_piece; ///< Optional captured piece + BoardState prev_state, next_state; ///< Board states before and after the move + + public: + MoveBuilder() = delete; + + /** + * @brief Constructs a MoveBuilder. + * @param board Board on which the move is applied + * @param move Move to build + */ + MoveBuilder(const Board& board, const Move& move); + + /** + * @brief Generates and returns the MoveExecution. + * @return MoveExecution representing all low-level effects of the move + */ + MoveExecution build(); + + private: + // Utilities for castling rights + void revoke_castling_if_rook_at(Square sq); ///< Revoke castling if rook moves or is captured + void revoke_castling_if_king_at(Square sq); ///< Revoke castling if king moves + + // Board state preparation + void prepare_base_state(); ///< Flip side, reset en passant, update half/full move counters + void prepare_next_state(); ///< Handle castling, en passant, commit state + + // Move effect steps + void remove_moving_piece(); ///< Removes moving piece from origin + void place_final_piece(); ///< Places final piece at destination + void handle_regular_capture(); ///< Adds effect for normal captures + void handle_en_passant_capture(); ///< Adds effect for en passant capture + void handle_promotion(); ///< Updates piece if promotion occurs + void handle_rook_castling(); ///< Handles rook movement during castling + void update_castling_rights(); ///< Updates castling rights if king/rook moves or rook captured + void update_en_passant_square(); ///< Sets en passant target square if pawn moved two squares + void commit_state(); ///< Records board state change in effects + void update_half_move_clock(); ///< Updates half-move counter for 50-move rule + void update_full_move_number(); ///< Updates full move number if black has moved + void flip_side_to_move(); ///< Flips active player + void reset_en_passant_square(); ///< Clears en passant square if move doesn't set one +}; diff --git a/include/bitbishop/moves/move_effect.hpp b/include/bitbishop/moves/move_effect.hpp new file mode 100644 index 0000000..25a61f4 --- /dev/null +++ b/include/bitbishop/moves/move_effect.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +/** + * @brief Represents a single low-level board modification. + * + * A chess move may consist of multiple effects: placing a piece, removing a + * piece, or updating board state (e.g., castling rights, en passant). Each + * MoveEffect describes exactly one such change and can be applied or reverted. + */ +struct MoveEffect { + /** + * @brief Type of board modification. + */ + enum class Type : uint8_t { Place, Remove, BoardState }; + + Type type; ///< Effect category + Square square = Squares::A1; ///< Target square (for Place/Remove) + Piece piece = Pieces::WHITE_KING; ///< Piece involved (for Place/Remove) + BoardState prev_state; ///< State before change (for BoardState effect) + BoardState next_state; ///< State after change (for BoardState effect) + + /** + * @brief Creates a piece placement effect. + * @param sq Destination square + * @param piece Piece to place + */ + static MoveEffect place(Square sq, Piece piece); + + /** + * @brief Creates a piece removal effect. + * @param sq Square to clear + * @param piece Piece being removed + */ + static MoveEffect remove(Square sq, Piece piece); + + /** + * @brief Creates a board state update effect. + * @param prev The previous state + * @param next The new state + */ + static MoveEffect state_change(const BoardState& prev, const BoardState& next); + + /** + * @brief Applies the effect to the board. + * @param board Board to modify + */ + void apply(Board& board) const; + + /** + * @brief Reverts the effect on the board. + * @param board Board to restore + */ + void revert(Board& board) const; +}; diff --git a/include/bitbishop/moves/move_execution.hpp b/include/bitbishop/moves/move_execution.hpp new file mode 100644 index 0000000..11cd4d8 --- /dev/null +++ b/include/bitbishop/moves/move_execution.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +/** + * @brief Aggregates the individual effects of a single move. + * + * A chess move may consist of several low-level effects + * (piece placements/removals, board state updates). + * MoveExecution stores these in order, allowing the move + * to be applied and fully reverted. + */ +struct MoveExecution { + static constexpr int MAX_EFFECTS = 6; ///< Maximum number of effects per move + + std::array effects; ///< Ordered list of effects + int count = 0; ///< Number of effects currently stored + + /** + * @brief Adds a new effect to the execution. + * @param effect The effect to append + * @warning Exceeding MAX_EFFECTS is undefined behaviour + */ + void add(const MoveEffect& effect); + + /** + * @brief Applies all effects in order. + * @param board Board to modify + */ + void apply(Board& board) const; + + /** + * @brief Reverts all effects in reverse order. + * @param board Board to restore + */ + void revert(Board& board) const; +}; diff --git a/include/bitbishop/moves/position.hpp b/include/bitbishop/moves/position.hpp new file mode 100644 index 0000000..3029a67 --- /dev/null +++ b/include/bitbishop/moves/position.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +/** + * @brief Represents a game position, including board state 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. + */ +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; + + /** + * @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. + */ + 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 + } + + /** + * @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 + */ + 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.. + */ + 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 + */ + [[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 + */ + [[nodiscard]] bool can_unmake() const { return !move_execution_history.empty(); } +}; diff --git a/include/bitbishop/piece.hpp b/include/bitbishop/piece.hpp index 698c1e8..ed46e62 100644 --- a/include/bitbishop/piece.hpp +++ b/include/bitbishop/piece.hpp @@ -14,17 +14,33 @@ * By convention: * - White pieces are uppercase (P, N, B, R, Q, K) * - Black pieces are lowercase (p, n, b, r, q, k) - * - NO_PIECE represents an empty square */ class Piece { public: - /** @brief Enum for piece types */ + /** + * @brief Enum of all supported piece types. + * + * Stored in a compact underlying type to ensure `Piece` remains lightweight. + */ enum Type : std::uint8_t { PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING }; + /** + * @brief Number of distinct piece types. + * + * Useful for array sizing and iteration without magic numbers. + */ + static constexpr std::size_t TYPE_COUNT = 6; + private: + /** + * @brief The type of the piece (pawn, knight, etc.). + */ Type m_type; + + /** + * @brief The color of the piece (white or black). + */ Color m_color; - char m_symbol; public: /** @@ -32,71 +48,188 @@ class Piece { * @param type Piece type * @param color Piece color */ - constexpr Piece(Type type, Color color) : m_type(type), m_color(color) { ; } + constexpr Piece(Type type, Color color) : m_type(type), m_color(color) {} /** - * @brief Constructs a piece. - * @param character Type of the piece to build in character notation - * @throw std::invalid_argument when the Piece character is invalid + * @brief Constructs a piece from a character representation. + * + * Uppercase indicates white pieces, lowercase indicates black pieces. + * + * @param character Character representing the piece ('P', 'n', etc.) + * @throw std::invalid_argument If the character does not represent a valid piece */ constexpr Piece(char character) { - m_symbol = character; - switch (character) { // clang-format off - case 'P': m_type = PAWN; m_color = Color::WHITE; break; - case 'N': m_type = KNIGHT; m_color = Color::WHITE; break; - case 'B': m_type = BISHOP; m_color = Color::WHITE; break; - case 'R': m_type = ROOK; m_color = Color::WHITE; break; - case 'Q': m_type = QUEEN; m_color = Color::WHITE; break; - case 'K': m_type = KING; m_color = Color::WHITE; break; - case 'p': m_type = PAWN; m_color = Color::BLACK; break; - case 'n': m_type = KNIGHT; m_color = Color::BLACK; break; - case 'b': m_type = BISHOP; m_color = Color::BLACK; break; - case 'r': m_type = ROOK; m_color = Color::BLACK; break; - case 'q': m_type = QUEEN; m_color = Color::BLACK; break; - case 'k': m_type = KING; m_color = Color::BLACK; break; - // clang-format on - default: - const std::string msg = std::format("Invalid piece character {}", character); - throw std::invalid_argument(msg); - } + m_type = Piece::type_from_char(character); + m_color = Piece::color_from_char(character); } /** - * @brief Returns the underlying enum type of the piece. - * @return Type of the piece + * @brief Returns the piece type. + * @return Type enum */ [[nodiscard]] constexpr Type type() const { return m_type; } /** - * @brief Returns the underlying enum color of the piece. - * @return Color of the piece + * @brief Returns the piece color. + * @return Color enum */ [[nodiscard]] constexpr Color color() const { return m_color; } /** * @brief Checks if the piece is white. - * @return true if the piece is white, false otherwise + * @return true if white, false otherwise */ [[nodiscard]] constexpr bool is_white() const { return m_color == Color::WHITE; } /** * @brief Checks if the piece is black. - * @return true if the piece is black, false otherwise + * @return true if black, false otherwise */ [[nodiscard]] constexpr bool is_black() const { return m_color == Color::BLACK; } + /** @brief Checks if the piece is a pawn. */ + [[nodiscard]] constexpr bool is_pawn() const { return m_type == Type::PAWN; } + /** @brief Checks if the piece is a knight. */ + [[nodiscard]] constexpr bool is_knight() const { return m_type == Type::KNIGHT; } + /** @brief Checks if the piece is a bishop. */ + [[nodiscard]] constexpr bool is_bishop() const { return m_type == Type::BISHOP; } + /** @brief Checks if the piece is a rook. */ + [[nodiscard]] constexpr bool is_rook() const { return m_type == Type::ROOK; } + /** @brief Checks if the piece is a queen. */ + [[nodiscard]] constexpr bool is_queen() const { return m_type == Type::QUEEN; } + /** @brief Checks if the piece is a king. */ + [[nodiscard]] constexpr bool is_king() const { return m_type == Type::KING; } + /** - * @brief Converts the piece to a printable character. - * @return 'P','N',...,'k' for pieces, '.' for NO_PIECE + * @brief Checks if the piece is a sliding piece (bishop, rook, queen). + * @return true if sliding, false otherwise */ - [[nodiscard]] constexpr char to_char() const { return m_symbol; }; + [[nodiscard]] constexpr bool is_slider() { + return m_type == Piece::BISHOP || m_type == Piece::ROOK || m_type == Piece::QUEEN; + } + + /** + * @brief Returns the type associated with a character representation. + * + * @param character Piece character ('P', 'b', etc.) + * @return Corresponding piece type + * @throw std::invalid_argument If the character is invalid + */ + [[nodiscard]] static constexpr Type type_from_char(char character) { + switch (character) { + case 'P': + case 'p': + return PAWN; + case 'N': + case 'n': + return KNIGHT; + case 'B': + case 'b': + return BISHOP; + case 'R': + case 'r': + return ROOK; + case 'Q': + case 'q': + return QUEEN; + case 'K': + case 'k': + return KING; + } + const std::string msg = std::format("Invalid piece character {}", character); + throw std::invalid_argument(msg); + } + + /** + * @brief Extracts piece color from character representation. + * + * @param character Piece character ('P' = white, 'p' = black) + * @return Detected color + * @throw std::invalid_argument If the character is not alphabetic + */ + [[nodiscard]] static constexpr Color color_from_char(char character) { + if (character >= 'A' && character <= 'Z') { + return Color::WHITE; + } + if (character >= 'a' && character <= 'z') { + return Color::BLACK; + } + const std::string msg = std::format("Invalid piece character {}", character); + throw std::invalid_argument(msg); + } + /** + * @brief Returns an array containing all valid piece types. + * + * Useful for iteration over all piece types in compile-time contexts. + * + * @return Array of piece types + */ + [[nodiscard]] static constexpr std::array all_types() { + return {PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING}; + } + + /** + * @brief Human-readable name of a piece type. + * + * @param type Piece type enum + * @return C-string name ("pawn", "knight", etc.) + */ + [[nodiscard]] static constexpr const char* name(Type type) { + switch (type) { + case PAWN: + return "pawn"; + case KNIGHT: + return "knight"; + case BISHOP: + return "bishop"; + case ROOK: + return "rook"; + case QUEEN: + return "queen"; + case KING: + return "king"; + } + return "unknown"; + } + + /** + * @brief Converts a type and color to its representative character. + * + * @param type Piece type + * @param color Piece color + * @return Uppercase for white, lowercase for black (e.g., 'Q' or 'q') + */ + [[nodiscard]] static constexpr char to_char(Type type, Color color) { + constexpr std::array whiteSymbols = {'P', 'N', 'B', 'R', 'Q', 'K'}; + constexpr std::array blackSymbols = {'p', 'n', 'b', 'r', 'q', 'k'}; + return (color == Color::WHITE) ? whiteSymbols[type] : blackSymbols[type]; + } + + /** + * @brief Converts the stored piece to its character representation. + * @return Character ('P', 'k', etc.) + */ + [[nodiscard]] constexpr char to_char() const { return Piece::to_char(m_type, m_color); } + + /** + * @brief Equality operator. + * @param other Piece to compare against + * @return true if both type and color match + */ constexpr bool operator==(const Piece& other) const { return m_type == other.m_type && m_color == other.m_color; } - constexpr bool operator!=(const Piece& other) const { return m_type != other.m_type || m_color != other.m_color; } + + /** + * @brief Inequality operator. + * @param other Piece to compare against + * @return true if type or color differ + */ + constexpr bool operator!=(const Piece& other) const { return !(*this == other); } }; namespace Pieces { -#define DEFINE_PIECE(name, ch) \ +#define DEFINE_PIECE(name, ch) \ + /** @brief Predefined piece constant for convenience. */ \ constexpr inline Piece name { ch } // White pieces diff --git a/src/bitbishop/board.cpp b/src/bitbishop/board.cpp index 0976b52..2c0cbdc 100644 --- a/src/bitbishop/board.cpp +++ b/src/bitbishop/board.cpp @@ -46,28 +46,28 @@ Board::Board(const std::string& fen) { // Second token: side to move iss >> token; - m_is_white_turn = (token == "w"); + m_state.m_is_white_turn = (token == "w"); // Third token: Castling Rights iss >> token; - m_white_castle_kingside = token.find('K') != std::string::npos; - m_white_castle_queenside = token.find('Q') != std::string::npos; - m_black_castle_kingside = token.find('k') != std::string::npos; - m_black_castle_queenside = token.find('q') != std::string::npos; + m_state.m_white_castle_kingside = token.find('K') != std::string::npos; + m_state.m_white_castle_queenside = token.find('Q') != std::string::npos; + m_state.m_black_castle_kingside = token.find('k') != std::string::npos; + m_state.m_black_castle_queenside = token.find('q') != std::string::npos; // Fourth token: en passant iss >> token; if (token == "-") { - m_en_passant_sq = std::nullopt; + m_state.m_en_passant_sq = std::nullopt; } else { - m_en_passant_sq = Square(token); + m_state.m_en_passant_sq = Square(token); } // Fifth token: Halfmove clock - iss >> m_halfmove_clock; + iss >> m_state.m_halfmove_clock; // Sixth token: Fullmove number - iss >> m_fullmove_number; + iss >> m_state.m_fullmove_number; } Bitboard Board::white_pieces() const { @@ -280,11 +280,12 @@ bool Board::operator==(const Board& other) const { m_w_knights == other.m_w_knights && m_w_king == other.m_w_king && m_w_queens == other.m_w_queens && m_b_pawns == other.m_b_pawns && m_b_rooks == other.m_b_rooks && m_b_bishops == other.m_b_bishops && m_b_knights == other.m_b_knights && m_b_king == other.m_b_king && m_b_queens == other.m_b_queens && - m_is_white_turn == other.m_is_white_turn && m_en_passant_sq == other.m_en_passant_sq && - m_white_castle_kingside == other.m_white_castle_kingside && - m_white_castle_queenside == other.m_white_castle_queenside && - m_black_castle_kingside == other.m_black_castle_kingside && - m_black_castle_queenside == other.m_black_castle_queenside; + m_state.m_is_white_turn == other.m_state.m_is_white_turn && + m_state.m_en_passant_sq == other.m_state.m_en_passant_sq && + m_state.m_white_castle_kingside == other.m_state.m_white_castle_kingside && + m_state.m_white_castle_queenside == other.m_state.m_white_castle_queenside && + m_state.m_black_castle_kingside == other.m_state.m_black_castle_kingside && + m_state.m_black_castle_queenside == other.m_state.m_black_castle_queenside; } bool Board::operator!=(const Board& other) const { return !this->operator==(other); } diff --git a/src/bitbishop/moves/move_builder.cpp b/src/bitbishop/moves/move_builder.cpp new file mode 100644 index 0000000..6b1b836 --- /dev/null +++ b/src/bitbishop/moves/move_builder.cpp @@ -0,0 +1,172 @@ +#include +#include + +MoveBuilder::MoveBuilder(const Board& board, const Move& move) : board(board), move(move) { + moving_piece = *board.get_piece(move.from); + opt_captured_piece = board.get_piece(move.to); + + final_piece = moving_piece; + + prev_state = board.get_state(); + next_state = board.get_state(); +} + +MoveExecution MoveBuilder::build() { + prepare_base_state(); + + remove_moving_piece(); + handle_regular_capture(); + handle_en_passant_capture(); + handle_promotion(); + place_final_piece(); + + prepare_next_state(); + + return effects; +} + +void MoveBuilder::prepare_base_state() { + flip_side_to_move(); + update_half_move_clock(); + update_full_move_number(); + reset_en_passant_square(); +} + +void MoveBuilder::prepare_next_state() { + handle_rook_castling(); + update_castling_rights(); + update_en_passant_square(); + commit_state(); +} + +void MoveBuilder::remove_moving_piece() { effects.add(MoveEffect::remove(move.from, moving_piece)); } + +void MoveBuilder::handle_regular_capture() { + if (opt_captured_piece && !move.is_en_passant) { + effects.add(MoveEffect::remove(move.to, *opt_captured_piece)); + } +} + +void MoveBuilder::handle_en_passant_capture() { + if (!move.is_en_passant) { + return; + } + + using namespace Const; + int direction = prev_state.m_is_white_turn ? -BOARD_WIDTH : +BOARD_WIDTH; + Square en_passant_sq(move.to.value() + direction); + Piece captured_piece = *board.get_piece(en_passant_sq); + effects.add(MoveEffect::remove(en_passant_sq, captured_piece)); +} + +void MoveBuilder::handle_promotion() { + if (move.promotion) { + final_piece = *move.promotion; + } +} + +void MoveBuilder::place_final_piece() { effects.add(MoveEffect::place(move.to, final_piece)); } + +void MoveBuilder::handle_rook_castling() { + if (!move.is_castling) { + return; + } + + using namespace Const; + const bool is_kingside = move.to.value() > move.from.value(); + const int from_rank = move.from.rank(); + Color color = (prev_state.m_is_white_turn) ? Color::WHITE : Color::BLACK; + Piece rook_piece = Piece(Piece::Type::ROOK, color); + + if (is_kingside) { + Square rook_from = Square(FILE_H_IND, from_rank); + Square rook_to = Square(FILE_F_IND, from_rank); + + effects.add(MoveEffect::remove(rook_from, rook_piece)); + effects.add(MoveEffect::place(rook_to, rook_piece)); + } else { + Square rook_from = Square(FILE_A_IND, from_rank); + Square rook_to = Square(FILE_D_IND, from_rank); + + effects.add(MoveEffect::remove(rook_from, rook_piece)); + effects.add(MoveEffect::place(rook_to, rook_piece)); + } +} + +void MoveBuilder::revoke_castling_if_rook_at(Square sq) { + using namespace Squares; + + if (sq == A1) { + next_state.m_white_castle_queenside = false; + } + if (sq == H1) { + next_state.m_white_castle_kingside = false; + } + if (sq == A8) { + next_state.m_black_castle_queenside = false; + } + if (sq == H8) { + next_state.m_black_castle_kingside = false; + } +} + +void MoveBuilder::revoke_castling_if_king_at(Square sq) { + using namespace Squares; + + if (sq == E1) { + next_state.m_white_castle_queenside = false; + next_state.m_white_castle_kingside = false; + } + if (sq == E8) { + next_state.m_black_castle_queenside = false; + next_state.m_black_castle_kingside = false; + } +} + +void MoveBuilder::update_castling_rights() { + if (moving_piece.is_king()) { + revoke_castling_if_king_at(move.from); + } + + if (moving_piece.is_rook()) { + revoke_castling_if_rook_at(move.from); + } + + const bool is_rook_captured = opt_captured_piece ? (*opt_captured_piece).is_rook() : false; + if (is_rook_captured) { + revoke_castling_if_rook_at(move.to); + } +} + +void MoveBuilder::update_en_passant_square() { + if (!moving_piece.is_pawn()) { + return; + } + + const int delta_rank = move.to.rank() - move.from.rank(); + if (delta_rank == 2) { + next_state.m_en_passant_sq = Square(move.from.file(), move.from.rank() + 1); + } else if (delta_rank == -2) { + next_state.m_en_passant_sq = Square(move.from.file(), move.from.rank() - 1); + } +} + +void MoveBuilder::commit_state() { effects.add(MoveEffect::state_change(prev_state, next_state)); } + +void MoveBuilder::update_half_move_clock() { + if (move.is_capture || moving_piece.is_pawn()) { + next_state.m_halfmove_clock = 0; + } else { + next_state.m_halfmove_clock++; + } +} + +void MoveBuilder::update_full_move_number() { + if (!next_state.m_is_white_turn) { + next_state.m_fullmove_number++; + } +} + +void MoveBuilder::flip_side_to_move() { next_state.m_is_white_turn = !prev_state.m_is_white_turn; } + +void MoveBuilder::reset_en_passant_square() { next_state.m_en_passant_sq = std::nullopt; } diff --git a/src/bitbishop/moves/move_effect.cpp b/src/bitbishop/moves/move_effect.cpp new file mode 100644 index 0000000..aee0bf7 --- /dev/null +++ b/src/bitbishop/moves/move_effect.cpp @@ -0,0 +1,50 @@ +#include +#include + +MoveEffect MoveEffect::place(Square sq, Piece piece) { + return MoveEffect{.type = Type::Place, .square = sq, .piece = piece, .prev_state = {}, .next_state = {}}; +} + +MoveEffect MoveEffect::remove(Square sq, Piece piece) { + return MoveEffect{.type = Type::Remove, .square = sq, .piece = piece, .prev_state = {}, .next_state = {}}; +} + +MoveEffect MoveEffect::state_change(const BoardState& prev, const BoardState& next) { + return MoveEffect{.type = Type::BoardState, + .square = Squares::A1, // dummy value cause no default init on this object + .piece = Pieces::WHITE_KING, // dummy value cause no default init on this object + .prev_state = prev, + .next_state = next}; +} + +void MoveEffect::apply(Board& board) const { + switch (type) { + case Type::Place: + board.set_piece(square, piece); + break; + case Type::Remove: + board.remove_piece(square); + break; + case Type::BoardState: + board.set_state(next_state); + break; + default: + std::unreachable(); + } +} + +void MoveEffect::revert(Board& board) const { + switch (type) { + case Type::Place: + board.remove_piece(square); + break; + case Type::Remove: + board.set_piece(square, piece); + break; + case Type::BoardState: + board.set_state(prev_state); + break; + default: + std::unreachable(); + } +} diff --git a/src/bitbishop/moves/move_execution.cpp b/src/bitbishop/moves/move_execution.cpp new file mode 100644 index 0000000..8db3f6c --- /dev/null +++ b/src/bitbishop/moves/move_execution.cpp @@ -0,0 +1,15 @@ +#include + +void MoveExecution::add(const MoveEffect& effect) { effects[count++] = effect; } + +void MoveExecution::apply(Board& board) const { + for (int i = 0; i < count; ++i) { + effects[i].apply(board); + } +} + +void MoveExecution::revert(Board& board) const { + for (int i = count - 1; i >= 0; --i) { + effects[i].revert(board); + } +} diff --git a/src/bitbishop/moves/position.cpp b/src/bitbishop/moves/position.cpp new file mode 100644 index 0000000..0314c83 --- /dev/null +++ b/src/bitbishop/moves/position.cpp @@ -0,0 +1,17 @@ +#include +#include + +void Position::apply_move(const Move& move) { + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + move_execution_history.push_back(exec); + exec.apply(board); +} + +void Position::revert_move() { + if (can_unmake()) { + MoveExecution last_exec = move_execution_history.back(); + last_exec.revert(board); + move_execution_history.pop_back(); + } +} diff --git a/tests/bitbishop/board/test_b_board_state.cpp b/tests/bitbishop/board/test_b_board_state.cpp new file mode 100644 index 0000000..ea1eadf --- /dev/null +++ b/tests/bitbishop/board/test_b_board_state.cpp @@ -0,0 +1,328 @@ +#include + +#include +#include + +using namespace Squares; + +/** + * @test Identical board states are equal. + * @brief Confirms operator== returns true for identical states. + */ +TEST(BoardStateTest, IdenticalStatesEqual) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + EXPECT_TRUE(state1 == state2); + EXPECT_FALSE(state1 != state2); +} + +/** + * @test State equals itself. + * @brief Confirms operator== returns true when comparing state to itself. + */ +TEST(BoardStateTest, StateEqualsItself) { + BoardState state{.m_is_white_turn = true, + .m_en_passant_sq = E3, + .m_white_castle_kingside = false, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = false, + .m_halfmove_clock = 5, + .m_fullmove_number = 10}; + + EXPECT_TRUE(state == state); + EXPECT_FALSE(state != state); +} + +/** + * @test Different turn makes states unequal. + * @brief Confirms states differ when m_is_white_turn differs. + */ +TEST(BoardStateTest, DifferentTurnUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_is_white_turn = false; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Different en passant square makes states unequal. + * @brief Confirms states differ when m_en_passant_sq differs. + */ +TEST(BoardStateTest, DifferentEnPassantUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = E3, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_en_passant_sq = E6; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test En passant nullopt vs square makes states unequal. + * @brief Confirms states differ when one has en passant and other doesn't. + */ +TEST(BoardStateTest, EnPassantNulloptVsSquareUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_en_passant_sq = E3; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Different white kingside castling makes states unequal. + * @brief Confirms states differ when m_white_castle_kingside differs. + */ +TEST(BoardStateTest, DifferentWhiteKingsideCastlingUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_white_castle_kingside = false; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Different white queenside castling makes states unequal. + * @brief Confirms states differ when m_white_castle_queenside differs. + */ +TEST(BoardStateTest, DifferentWhiteQueensideCastlingUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_white_castle_queenside = false; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Different black kingside castling makes states unequal. + * @brief Confirms states differ when m_black_castle_kingside differs. + */ +TEST(BoardStateTest, DifferentBlackKingsideCastlingUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_black_castle_kingside = false; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Different black queenside castling makes states unequal. + * @brief Confirms states differ when m_black_castle_queenside differs. + */ +TEST(BoardStateTest, DifferentBlackQueensideCastlingUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_black_castle_queenside = false; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Different halfmove clock makes states unequal. + * @brief Confirms states differ when m_halfmove_clock differs. + */ +TEST(BoardStateTest, DifferentHalfmoveClockUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_halfmove_clock = 10; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Different fullmove number makes states unequal. + * @brief Confirms states differ when m_fullmove_number differs. + */ +TEST(BoardStateTest, DifferentFullmoveNumberUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2 = state1; + state2.m_fullmove_number = 20; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Multiple differences make states unequal. + * @brief Confirms states differ when multiple fields differ. + */ +TEST(BoardStateTest, MultipleDifferencesUnequal) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = std::nullopt, + .m_white_castle_kingside = true, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = true, + .m_halfmove_clock = 0, + .m_fullmove_number = 1}; + + BoardState state2{.m_is_white_turn = false, + .m_en_passant_sq = E6, + .m_white_castle_kingside = false, + .m_white_castle_queenside = false, + .m_black_castle_kingside = false, + .m_black_castle_queenside = false, + .m_halfmove_clock = 50, + .m_fullmove_number = 100}; + + EXPECT_FALSE(state1 == state2); + EXPECT_TRUE(state1 != state2); +} + +/** + * @test Equality is symmetric. + * @brief Confirms that if state1 == state2, then state2 == state1. + */ +TEST(BoardStateTest, EqualityIsSymmetric) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = E3, + .m_white_castle_kingside = true, + .m_white_castle_queenside = false, + .m_black_castle_kingside = false, + .m_black_castle_queenside = true, + .m_halfmove_clock = 5, + .m_fullmove_number = 10}; + + BoardState state2 = state1; + + EXPECT_TRUE(state1 == state2); + EXPECT_TRUE(state2 == state1); +} + +/** + * @test Inequality is symmetric. + * @brief Confirms that if state1 != state2, then state2 != state1. + */ +TEST(BoardStateTest, InequalityIsSymmetric) { + BoardState state1{.m_is_white_turn = true, + .m_en_passant_sq = E3, + .m_white_castle_kingside = true, + .m_white_castle_queenside = false, + .m_black_castle_kingside = false, + .m_black_castle_queenside = true, + .m_halfmove_clock = 5, + .m_fullmove_number = 10}; + + BoardState state2 = state1; + state2.m_is_white_turn = false; + + EXPECT_TRUE(state1 != state2); + EXPECT_TRUE(state2 != state1); +} + +/** + * @test Copy produces equal state. + * @brief Confirms copy-constructed state is equal to original. + */ +TEST(BoardStateTest, CopyProducesEqualState) { + BoardState state1{.m_is_white_turn = false, + .m_en_passant_sq = D6, + .m_white_castle_kingside = false, + .m_white_castle_queenside = true, + .m_black_castle_kingside = true, + .m_black_castle_queenside = false, + .m_halfmove_clock = 25, + .m_fullmove_number = 50}; + + BoardState state2 = state1; + + EXPECT_TRUE(state1 == state2); + EXPECT_FALSE(state1 != state2); +} diff --git a/tests/bitbishop/moves/test_move_builder.cpp b/tests/bitbishop/moves/test_move_builder.cpp new file mode 100644 index 0000000..a475e46 --- /dev/null +++ b/tests/bitbishop/moves/test_move_builder.cpp @@ -0,0 +1,668 @@ +#include + +#include +#include +#include +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +TEST(MoveBuilderTest, QuietMoveGeneratesCorrectEffects) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + Move move{E2, E4}; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + ASSERT_EQ(exec.count, 3); // remove + place + state_change + + // Apply effects and check board change + exec.apply(board); + + auto p_from = board.get_piece(E2); + auto p_to = board.get_piece(E4); + + EXPECT_FALSE(p_from.has_value()); + ASSERT_TRUE(p_to.has_value()); + EXPECT_EQ(*p_to, WHITE_PAWN); + + // Revert and check restoration + exec.revert(board); + auto pf = board.get_piece(E2); + EXPECT_TRUE(pf); + EXPECT_TRUE(*pf == WHITE_PAWN); + EXPECT_FALSE(board.get_piece(E4).has_value()); +} + +TEST(MoveBuilderTest, CaptureMoveGeneratesCorrectEffects) { + Board board = Board::Empty(); + board.set_piece(E5, BLACK_KNIGHT); + board.set_piece(C3, WHITE_BISHOP); + + Move move{.from = C3, .to = E5, .is_capture = true, .is_en_passant = false, .is_castling = false}; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + // Should remove bishop from C3, remove knight from E5, place bishop on E5, + state change + ASSERT_EQ(exec.count, 4); // remove-moving, remove-captured, place-final, state-change + + exec.apply(board); + + EXPECT_FALSE(board.get_piece(C3)); + ASSERT_TRUE(board.get_piece(E5)); + EXPECT_EQ(*board.get_piece(E5), WHITE_BISHOP); + + exec.revert(board); + + EXPECT_TRUE(board.get_piece(C3)); + EXPECT_TRUE(*board.get_piece(C3) == WHITE_BISHOP); + EXPECT_TRUE(board.get_piece(E5)); + EXPECT_TRUE(*board.get_piece(E5) == BLACK_KNIGHT); +} + +TEST(MoveBuilderTest, EnPassantCaptureCreatesCorrectEffect) { + Board board = Board::Empty(); + + board.set_piece(E5, WHITE_PAWN); + // Black pawn moved from D7 -> D5 to allow en passant at D6 + board.set_piece(D5, BLACK_PAWN); + + BoardState st = board.get_state(); + st.m_en_passant_sq = D6; + st.m_is_white_turn = true; + board.set_state(st); + + Move move{E5, D6}; + move.is_en_passant = true; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + // Expected effects: + // remove E5 + // remove D5 (captured pawn) + // place D6 (white pawn) + // state change + ASSERT_EQ(exec.count, 4); + + exec.apply(board); + + EXPECT_FALSE(board.get_piece(E5)); + EXPECT_FALSE(board.get_piece(D5)); // captured + ASSERT_TRUE(board.get_piece(D6)); + EXPECT_EQ(*board.get_piece(D6), WHITE_PAWN); + + exec.revert(board); + + EXPECT_TRUE(board.get_piece(E5)); + EXPECT_TRUE(*board.get_piece(E5) == WHITE_PAWN); + EXPECT_TRUE(board.get_piece(D5)); + EXPECT_TRUE(*board.get_piece(D5) == BLACK_PAWN); + EXPECT_FALSE(board.get_piece(D6)); +} + +TEST(MoveBuilderTest, EnPassantIsResetIfNotUsed) { + Board board = Board::Empty(); + + board.set_piece(E5, WHITE_PAWN); + // Black pawn moved from D7 -> D5 to allow en passant at D6 + board.set_piece(D5, BLACK_PAWN); + board.set_piece(G1, WHITE_KNIGHT); + + BoardState st = board.get_state(); + st.m_en_passant_sq = D6; + st.m_is_white_turn = true; + board.set_state(st); + + Move move{G1, F3}; + move.is_en_passant = false; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + exec.apply(board); + + EXPECT_EQ(board.get_state().m_en_passant_sq, std::nullopt); + + exec.revert(board); + + EXPECT_TRUE(board.get_state().m_en_passant_sq); + EXPECT_EQ(board.get_state().m_en_passant_sq, D6); +} + +TEST(MoveBuilderTest, PromotionCreatesCorrectEffects) { + Board board = Board::Empty(); + board.set_piece(E7, WHITE_PAWN); + + Move move{E7, E8}; + move.promotion = WHITE_QUEEN; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + ASSERT_EQ(exec.count, 3); // remove pawn, place queen, state-change + + exec.apply(board); + + EXPECT_FALSE(board.get_piece(E7)); + ASSERT_TRUE(board.get_piece(E8)); + EXPECT_EQ(*board.get_piece(E8), WHITE_QUEEN); + + exec.revert(board); + + EXPECT_TRUE(board.get_piece(E7)); + EXPECT_TRUE(*board.get_piece(E7) == WHITE_PAWN); + EXPECT_FALSE(board.get_piece(E8)); +} + +TEST(MoveBuilderTest, PromotionWithCaptureCreatesCorrectEffects) { + Board board = Board::Empty(); + board.set_piece(E7, WHITE_PAWN); + board.set_piece(F8, BLACK_QUEEN); + + Move move{E7, F8}; + move.promotion = WHITE_QUEEN; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + // remove pawn, remove captured, place queen, state-change + ASSERT_EQ(exec.count, 4); + + exec.apply(board); + + EXPECT_FALSE(board.get_piece(E7)); + ASSERT_TRUE(board.get_piece(F8)); + EXPECT_EQ(*board.get_piece(F8), WHITE_QUEEN); + + exec.revert(board); + + EXPECT_TRUE(board.get_piece(E7)); + EXPECT_TRUE(*board.get_piece(E7) == WHITE_PAWN); + EXPECT_TRUE(board.get_piece(F8)); + EXPECT_TRUE(*board.get_piece(F8) == BLACK_QUEEN); +} + +TEST(MoveBuilderTest, CastlingKingsideWhitesGeneratesRookMove) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(H1, WHITE_ROOK); + + BoardState st = board.get_state(); + st.m_white_castle_kingside = true; + st.m_white_castle_queenside = true; + st.m_is_white_turn = true; + board.set_state(st); + + Move move{E1, G1}; + move.is_castling = true; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + // Effects: + // king remove E1, king place G1 + // rook remove H1, rook place F1 + // state change + ASSERT_EQ(exec.count, 5); + + exec.apply(board); + + // King moved + EXPECT_EQ(*board.get_piece(G1), WHITE_KING); + EXPECT_FALSE(board.get_piece(E1)); + + // Rook moved + EXPECT_EQ(*board.get_piece(F1), WHITE_ROOK); + EXPECT_FALSE(board.get_piece(H1)); + + exec.revert(board); + + EXPECT_EQ(*board.get_piece(E1), WHITE_KING); + EXPECT_EQ(*board.get_piece(H1), WHITE_ROOK); +} + +TEST(MoveBuilderTest, CastlingQueensideWhitesGeneratesRookMove) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + board.set_piece(A1, WHITE_ROOK); + + BoardState st = board.get_state(); + st.m_white_castle_kingside = true; + st.m_white_castle_queenside = true; + st.m_is_white_turn = true; + board.set_state(st); + + Move move{E1, C1}; + move.is_castling = true; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + // Effects: + // king remove E1, king place C1 + // rook remove A1, rook place D1 + // state change + ASSERT_EQ(exec.count, 5); + + exec.apply(board); + + // King moved + EXPECT_EQ(*board.get_piece(C1), WHITE_KING); + EXPECT_FALSE(board.get_piece(E1)); + + // Rook moved + EXPECT_EQ(*board.get_piece(D1), WHITE_ROOK); + EXPECT_FALSE(board.get_piece(A1)); + + exec.revert(board); + + EXPECT_EQ(*board.get_piece(E1), WHITE_KING); + EXPECT_EQ(*board.get_piece(A1), WHITE_ROOK); +} + +TEST(MoveBuilderTest, CastlingKingsideBlacksGeneratesRookMove) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + board.set_piece(H8, BLACK_ROOK); + + BoardState st = board.get_state(); + st.m_black_castle_kingside = true; + st.m_black_castle_queenside = true; + st.m_is_white_turn = false; + board.set_state(st); + + Move move{E8, G8}; + move.is_castling = true; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + // Effects: + // king remove E8, king place G8 + // rook remove H8, rook place F8 + // state change + ASSERT_EQ(exec.count, 5); + + exec.apply(board); + + // King moved + EXPECT_EQ(*board.get_piece(G8), BLACK_KING); + EXPECT_FALSE(board.get_piece(E8)); + + // Rook moved + EXPECT_EQ(*board.get_piece(F8), BLACK_ROOK); + EXPECT_FALSE(board.get_piece(H8)); + + exec.revert(board); + + EXPECT_EQ(*board.get_piece(E8), BLACK_KING); + EXPECT_EQ(*board.get_piece(H8), BLACK_ROOK); +} + +TEST(MoveBuilderTest, CastlingQueensideBlacksGeneratesRookMove) { + Board board = Board::Empty(); + board.set_piece(E8, BLACK_KING); + board.set_piece(A8, BLACK_ROOK); + + BoardState st = board.get_state(); + st.m_black_castle_kingside = true; + st.m_black_castle_queenside = true; + st.m_is_white_turn = false; + board.set_state(st); + + Move move{E8, C8}; + move.is_castling = true; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + // Effects: + // king remove E8, king place C8 + // rook remove A8, rook place D8 + // state change + ASSERT_EQ(exec.count, 5); + + exec.apply(board); + + // King moved + EXPECT_EQ(*board.get_piece(C8), BLACK_KING); + EXPECT_FALSE(board.get_piece(E8)); + + // Rook moved + EXPECT_EQ(*board.get_piece(D8), BLACK_ROOK); + EXPECT_FALSE(board.get_piece(A8)); + + exec.revert(board); + + EXPECT_EQ(*board.get_piece(E8), BLACK_KING); + EXPECT_EQ(*board.get_piece(A8), BLACK_ROOK); +} + +TEST(MoveBuilderTest, DoublePawnPushSetsEnPassantSquare) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + Move move{E2, E4}; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + exec.apply(board); + + BoardState st = board.get_state(); + ASSERT_TRUE(st.m_en_passant_sq.has_value()); + EXPECT_EQ(st.m_en_passant_sq.value(), E3); + + exec.revert(board); + + EXPECT_TRUE(board.get_piece(E2)); + EXPECT_EQ(board.get_piece(E2), WHITE_PAWN); + EXPECT_FALSE(board.get_state().m_en_passant_sq.has_value()); +} + +TEST(MoveBuilderTest, KingMoveRevokesCastlingRights) { + Board board = Board::Empty(); + board.set_piece(E1, WHITE_KING); + + BoardState st = board.get_state(); + st.m_white_castle_kingside = true; + st.m_white_castle_queenside = true; + board.set_state(st); + + Move move{E1, E2}; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + exec.apply(board); + + BoardState final_state = board.get_state(); + + EXPECT_TRUE(board.get_piece(E2)); + EXPECT_EQ(board.get_piece(E2), WHITE_KING); + EXPECT_FALSE(board.get_piece(E1)); + EXPECT_FALSE(final_state.m_white_castle_kingside); + EXPECT_FALSE(final_state.m_white_castle_queenside); + + exec.revert(board); + + BoardState reverted = board.get_state(); + + EXPECT_TRUE(board.get_piece(E1)); + EXPECT_EQ(board.get_piece(E1), WHITE_KING); + EXPECT_FALSE(board.get_piece(E2)); + EXPECT_TRUE(reverted.m_white_castle_kingside); + EXPECT_TRUE(reverted.m_white_castle_queenside); +} + +TEST(MoveBuilderTest, RookMoveRevokesCastlingRights) { + Board board = Board::Empty(); + board.set_piece(A1, WHITE_ROOK); + + BoardState st = board.get_state(); + st.m_white_castle_queenside = true; + board.set_state(st); + + Move move{A1, A2}; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + exec.apply(board); + + EXPECT_TRUE(board.get_piece(A2)); + EXPECT_EQ(board.get_piece(A2), WHITE_ROOK); + EXPECT_FALSE(board.get_piece(A1)); + EXPECT_FALSE(board.get_state().m_white_castle_queenside); + + exec.revert(board); + + EXPECT_TRUE(board.get_piece(A1)); + EXPECT_EQ(board.get_piece(A1), WHITE_ROOK); + EXPECT_FALSE(board.get_piece(A2)); + EXPECT_TRUE(board.get_state().m_white_castle_queenside); +} + +TEST(MoveBuilderTest, HalfMoveClockResetsOnPawnMove) { + Board board = Board::Empty(); + BoardState st = board.get_state(); + st.m_halfmove_clock = 7; + board.set_state(st); + + board.set_piece(E2, WHITE_PAWN); + + Move move{E2, E3}; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + exec.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, 0); +} + +TEST(MoveBuilderTest, HalfMoveClockResetsOnCapture) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_ROOK); + board.set_piece(C4, BLACK_BISHOP); + + BoardState st = board.get_state(); + st.m_halfmove_clock = 7; + board.set_state(st); + + Move move = Move::make(E4, C4, true); + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + exec.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, 0); +} + +TEST(MoveBuilderTest, HalfMoveClockIncrementsOnQuietMove) { + Board board = Board::Empty(); + board.set_piece(G1, WHITE_KNIGHT); + + BoardState st = board.get_state(); + st.m_halfmove_clock = 5; + st.m_is_white_turn = true; + board.set_state(st); + + Move move = Move::make(G1, F3, false); // quiet knight move + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + exec.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, st.m_halfmove_clock + 1); +} + +TEST(MoveBuilderTest, HalfMoveClockIncrementsOverTwoQuietMoves) { + Board board = Board::Empty(); + board.set_piece(G1, WHITE_KNIGHT); + board.set_piece(G8, BLACK_KNIGHT); + + BoardState st = board.get_state(); + st.m_halfmove_clock = 2; + st.m_is_white_turn = true; + board.set_state(st); + + // White quiet move + Move wmove = Move::make(G1, F3, false); + MoveBuilder wbuilder(board, wmove); + MoveExecution wexec = wbuilder.build(); + wexec.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, st.m_halfmove_clock + 1); + + // Black quiet move + Move bmove = Move::make(G8, F6, false); + MoveBuilder bbuilder(board, bmove); + MoveExecution bexec = bbuilder.build(); + bexec.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, st.m_halfmove_clock + 2); +} + +TEST(MoveExecutionTest, HalfMoveClockRestoredOnRevert) { + Board board = Board::Empty(); + board.set_piece(G1, WHITE_KNIGHT); + + BoardState before = board.get_state(); + before.m_halfmove_clock = 12; + before.m_is_white_turn = true; + board.set_state(before); + + Move move = Move::make(G1, F3, false); + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + exec.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, before.m_halfmove_clock + 1); + + exec.revert(board); + + BoardState after = board.get_state(); + EXPECT_EQ(after.m_halfmove_clock, before.m_halfmove_clock); + EXPECT_EQ(after.m_is_white_turn, before.m_is_white_turn); +} + +TEST(MoveBuilderTest, HalfMoveClockIncrementThenResetOnCapture) { + Board board = Board::Empty(); + board.set_piece(G1, WHITE_KNIGHT); + board.set_piece(E4, BLACK_BISHOP); + + BoardState st = board.get_state(); + st.m_halfmove_clock = 0; + st.m_is_white_turn = true; + board.set_state(st); + + // Quiet knight move + { + Move move1 = Move::make(G1, F3, false); + MoveBuilder builder1(board, move1); + MoveExecution exec1 = builder1.build(); + exec1.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, st.m_halfmove_clock + 1); + } + + // Capture bishop + { + Move move2 = Move::make(F3, E5, true); + MoveBuilder builder2(board, move2); + MoveExecution exec2 = builder2.build(); + exec2.apply(board); + + EXPECT_EQ(board.get_state().m_halfmove_clock, 0); + } +} + +TEST(MoveExecutionTest, SideToMoveFlipsCorrectly) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + BoardState prev_state = board.get_state(); + prev_state.m_is_white_turn = true; + board.set_state(prev_state); + + Move move{E2, E3}; + + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + EXPECT_TRUE(board.get_state().m_is_white_turn); + + exec.apply(board); + + EXPECT_FALSE(board.get_state().m_is_white_turn); +} + +TEST(MoveBuilderTest, FullMoveIncrementsOnWhiteMove) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + BoardState st = board.get_state(); + st.m_fullmove_number = 10; + st.m_is_white_turn = true; + board.set_state(st); + + Move move = Move::make(E2, E4, false); + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + exec.apply(board); + + EXPECT_EQ(board.get_state().m_fullmove_number, st.m_fullmove_number + 1); +} + +TEST(MoveBuilderTest, FullMoveDoesNotIncrementOnBlackMove) { + Board board = Board::Empty(); + board.set_piece(E7, BLACK_PAWN); + + BoardState st = board.get_state(); + st.m_fullmove_number = 10; + st.m_is_white_turn = false; + board.set_state(st); + + Move move = Move::make(E7, E5, false); + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + exec.apply(board); + + EXPECT_EQ(board.get_state().m_fullmove_number, st.m_fullmove_number); +} + +TEST(MoveBuilderTest, FullMoveNumberSequentialTurns) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + board.set_piece(E7, BLACK_PAWN); + + BoardState st = board.get_state(); + st.m_fullmove_number = 4; + st.m_is_white_turn = true; + board.set_state(st); + + // White moves -> should increment to 5 + Move wmove = Move::make(E2, E4, false); + MoveBuilder wbuild(board, wmove); + MoveExecution wexec = wbuild.build(); + wexec.apply(board); + EXPECT_EQ(board.get_state().m_fullmove_number, st.m_fullmove_number + 1); + + // Black moves -> no increment + Move bmove = Move::make(E7, E5, false); + MoveBuilder bbuild(board, bmove); + MoveExecution bexec = bbuild.build(); + bexec.apply(board); + EXPECT_EQ(board.get_state().m_fullmove_number, st.m_fullmove_number + 1); +} + +TEST(MoveExecutionTest, FullMoveNumberRestoredOnRevert) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + BoardState before = board.get_state(); + before.m_fullmove_number = 6; + before.m_is_white_turn = true; + board.set_state(before); + + Move move = Move::make(E2, E4, false); + MoveBuilder builder(board, move); + MoveExecution exec = builder.build(); + + exec.apply(board); + ASSERT_EQ(board.get_state().m_fullmove_number, before.m_fullmove_number + 1); + + exec.revert(board); + + EXPECT_EQ(board.get_state().m_fullmove_number, before.m_fullmove_number); + EXPECT_EQ(board.get_state().m_is_white_turn, before.m_is_white_turn); +} diff --git a/tests/bitbishop/moves/test_move_effect.cpp b/tests/bitbishop/moves/test_move_effect.cpp new file mode 100644 index 0000000..44618e4 --- /dev/null +++ b/tests/bitbishop/moves/test_move_effect.cpp @@ -0,0 +1,407 @@ +#include + +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +/** + * @test Place effect factory creates correct effect. + * @brief Confirms MoveEffect::place() creates effect with correct type and data. + */ +TEST(MoveEffectTest, PlaceEffectFactory) { + MoveEffect effect = MoveEffect::place(E4, WHITE_PAWN); + + EXPECT_EQ(effect.type, MoveEffect::Type::Place); + EXPECT_EQ(effect.square, E4); + EXPECT_EQ(effect.piece, WHITE_PAWN); +} + +/** + * @test Remove effect factory creates correct effect. + * @brief Confirms MoveEffect::remove() creates effect with correct type and data. + */ +TEST(MoveEffectTest, RemoveEffectFactory) { + MoveEffect effect = MoveEffect::remove(E4, BLACK_KNIGHT); + + EXPECT_EQ(effect.type, MoveEffect::Type::Remove); + EXPECT_EQ(effect.square, E4); + EXPECT_EQ(effect.piece, BLACK_KNIGHT); +} + +/** + * @test State change effect factory creates correct effect. + * @brief Confirms MoveEffect::state_change() creates effect with correct type and states. + */ +TEST(MoveEffectTest, StateChangeEffectFactory) { + Board board1 = Board::StartingPosition(); + Board board2 = Board::Empty(); + + BoardState prev = board1.get_state(); + BoardState next = board2.get_state(); + + MoveEffect effect = MoveEffect::state_change(prev, next); + + EXPECT_EQ(effect.type, MoveEffect::Type::BoardState); + // States should be stored (exact comparison depends on BoardState implementation) +} + +/** + * @test Apply place effect adds piece to board. + * @brief Confirms MoveEffect::apply() places piece on board for Place type. + */ +TEST(MoveEffectTest, ApplyPlaceEffect) { + Board board = Board::Empty(); + MoveEffect effect = MoveEffect::place(E4, WHITE_PAWN); + + effect.apply(board); + + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); +} + +/** + * @test Apply remove effect removes piece from board. + * @brief Confirms MoveEffect::apply() removes piece from board for Remove type. + */ +TEST(MoveEffectTest, ApplyRemoveEffect) { + Board board = Board::Empty(); + board.set_piece(E4, BLACK_ROOK); + + MoveEffect effect = MoveEffect::remove(E4, BLACK_ROOK); + effect.apply(board); + + EXPECT_EQ(board.get_piece(E4), std::nullopt); +} + +/** + * @test Apply state change effect updates board state. + * @brief Confirms MoveEffect::apply() updates board state for BoardState type. + */ +TEST(MoveEffectTest, ApplyStateChangeEffect) { + Board board = Board::Empty(); + Board target = Board::StartingPosition(); + + BoardState prev = board.get_state(); + BoardState next = target.get_state(); + + MoveEffect effect = MoveEffect::state_change(prev, next); + effect.apply(board); + + EXPECT_EQ(board.get_state(), next); +} + +/** + * @test Revert place effect removes piece. + * @brief Confirms MoveEffect::revert() removes placed piece. + */ +TEST(MoveEffectTest, RevertPlaceEffect) { + Board board = Board::Empty(); + MoveEffect effect = MoveEffect::place(E4, WHITE_PAWN); + + effect.apply(board); + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); + + effect.revert(board); + EXPECT_EQ(board.get_piece(E4), std::nullopt); +} + +/** + * @test Revert remove effect restores piece. + * @brief Confirms MoveEffect::revert() restores removed piece. + */ +TEST(MoveEffectTest, RevertRemoveEffect) { + Board board = Board::Empty(); + board.set_piece(E4, BLACK_ROOK); + + MoveEffect effect = MoveEffect::remove(E4, BLACK_ROOK); + + effect.apply(board); + EXPECT_EQ(board.get_piece(E4), std::nullopt); + + effect.revert(board); + EXPECT_EQ(board.get_piece(E4), BLACK_ROOK); +} + +/** + * @test Revert state change effect restores previous state. + * @brief Confirms MoveEffect::revert() restores previous board state. + */ +TEST(MoveEffectTest, RevertStateChangeEffect) { + Board board = Board::Empty(); + Board target = Board::StartingPosition(); + + BoardState prev = board.get_state(); + BoardState next = target.get_state(); + + MoveEffect effect = MoveEffect::state_change(prev, next); + + effect.apply(board); + EXPECT_EQ(board.get_state(), next); + + effect.revert(board); + EXPECT_EQ(board.get_state(), prev); +} + +/** + * @test Apply then revert place effect is identity. + * @brief Confirms apply followed by revert returns board to original state + * for place effect. + */ +TEST(MoveEffectTest, ApplyRevertPlaceIsIdentity) { + Board board = Board::Empty(); + Board original = board; + + MoveEffect effect = MoveEffect::place(E4, WHITE_PAWN); + + effect.apply(board); + effect.revert(board); + + EXPECT_EQ(board, original); +} + +/** + * @test Apply then revert remove effect is identity. + * @brief Confirms apply followed by revert returns board to original state + * for remove effect. + */ +TEST(MoveEffectTest, ApplyRevertRemoveIsIdentity) { + Board board = Board::Empty(); + board.set_piece(E4, BLACK_ROOK); + Board original = board; + + MoveEffect effect = MoveEffect::remove(E4, BLACK_ROOK); + + effect.apply(board); + effect.revert(board); + + EXPECT_EQ(board, original); +} + +/** + * @test Apply then revert state change is identity. + * @brief Confirms apply followed by revert returns board to original state + * for state change effect. + */ +TEST(MoveEffectTest, ApplyRevertStateChangeIsIdentity) { + Board board = Board::Empty(); + Board target = Board::StartingPosition(); + + BoardState prev = board.get_state(); + BoardState next = target.get_state(); + + Board original = board; + + MoveEffect effect = MoveEffect::state_change(prev, next); + + effect.apply(board); + effect.revert(board); + + EXPECT_EQ(board, original); +} + +/** + * @test Multiple place effects can be applied. + * @brief Confirms multiple place effects can be applied sequentially. + */ +TEST(MoveEffectTest, MultiplePlaceEffects) { + Board board = Board::Empty(); + + MoveEffect effect1 = MoveEffect::place(E4, WHITE_PAWN); + MoveEffect effect2 = MoveEffect::place(D4, BLACK_PAWN); + MoveEffect effect3 = MoveEffect::place(E5, WHITE_KNIGHT); + + effect1.apply(board); + effect2.apply(board); + effect3.apply(board); + + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); + EXPECT_EQ(board.get_piece(D4), BLACK_PAWN); + EXPECT_EQ(board.get_piece(E5), WHITE_KNIGHT); +} + +/** + * @test Multiple effects can be reverted in reverse order. + * @brief Confirms multiple effects can be reverted in LIFO order. + */ +TEST(MoveEffectTest, MultipleEffectsRevertedInReverse) { + Board board = Board::Empty(); + Board original = board; + + MoveEffect effect1 = MoveEffect::place(E4, WHITE_PAWN); + MoveEffect effect2 = MoveEffect::place(D4, BLACK_PAWN); + MoveEffect effect3 = MoveEffect::place(E5, WHITE_KNIGHT); + + effect1.apply(board); + effect2.apply(board); + effect3.apply(board); + + effect3.revert(board); + effect2.revert(board); + effect1.revert(board); + + EXPECT_EQ(board, original); +} + +/** + * @test Place effect on occupied square. + * @brief Confirms place effect overwrites existing piece. + */ +TEST(MoveEffectTest, PlaceEffectOnOccupiedSquare) { + Board board = Board::Empty(); + board.set_piece(E4, BLACK_PAWN); + + MoveEffect effect = MoveEffect::place(E4, WHITE_QUEEN); + effect.apply(board); + + EXPECT_EQ(board.get_piece(E4), WHITE_QUEEN); +} + +/** + * @test Remove effect on empty square. + * @brief Confirms remove effect on empty square is handled gracefully. + */ +TEST(MoveEffectTest, RemoveEffectOnEmptySquare) { + Board board = Board::Empty(); + + MoveEffect effect = MoveEffect::remove(E4, BLACK_PAWN); + effect.apply(board); + + EXPECT_EQ(board.get_piece(E4), std::nullopt); +} + +/** + * @test Place different piece types. + * @brief Confirms place effect works for all piece types. + */ +TEST(MoveEffectTest, PlaceDifferentPieceTypes) { + Board board = Board::Empty(); + + MoveEffect pawn = MoveEffect::place(A2, WHITE_PAWN); + MoveEffect knight = MoveEffect::place(B1, WHITE_KNIGHT); + MoveEffect bishop = MoveEffect::place(C1, WHITE_BISHOP); + MoveEffect rook = MoveEffect::place(A1, WHITE_ROOK); + MoveEffect queen = MoveEffect::place(D1, WHITE_QUEEN); + MoveEffect king = MoveEffect::place(E1, WHITE_KING); + + pawn.apply(board); + knight.apply(board); + bishop.apply(board); + rook.apply(board); + queen.apply(board); + king.apply(board); + + EXPECT_EQ(board.get_piece(A2), WHITE_PAWN); + EXPECT_EQ(board.get_piece(B1), WHITE_KNIGHT); + EXPECT_EQ(board.get_piece(C1), WHITE_BISHOP); + EXPECT_EQ(board.get_piece(A1), WHITE_ROOK); + EXPECT_EQ(board.get_piece(D1), WHITE_QUEEN); + EXPECT_EQ(board.get_piece(E1), WHITE_KING); +} + +/** + * @test Remove different piece types. + * @brief Confirms remove effect works for all piece types. + */ +TEST(MoveEffectTest, RemoveDifferentPieceTypes) { + Board board = Board::StartingPosition(); + + MoveEffect pawn = MoveEffect::remove(E2, WHITE_PAWN); + MoveEffect knight = MoveEffect::remove(B1, WHITE_KNIGHT); + MoveEffect bishop = MoveEffect::remove(C1, WHITE_BISHOP); + MoveEffect rook = MoveEffect::remove(A1, WHITE_ROOK); + MoveEffect queen = MoveEffect::remove(D1, WHITE_QUEEN); + + pawn.apply(board); + knight.apply(board); + bishop.apply(board); + rook.apply(board); + queen.apply(board); + + EXPECT_EQ(board.get_piece(E2), std::nullopt); + EXPECT_EQ(board.get_piece(B1), std::nullopt); + EXPECT_EQ(board.get_piece(C1), std::nullopt); + EXPECT_EQ(board.get_piece(A1), std::nullopt); + EXPECT_EQ(board.get_piece(D1), std::nullopt); +} + +/** + * @test Place effect on all board squares. + * @brief Confirms place effect works on corner and edge squares. + */ +TEST(MoveEffectTest, PlaceEffectOnAllBoardSquares) { + Board board = Board::Empty(); + + // Test corners + MoveEffect corner1 = MoveEffect::place(A1, WHITE_ROOK); + MoveEffect corner2 = MoveEffect::place(H1, WHITE_ROOK); + MoveEffect corner3 = MoveEffect::place(A8, BLACK_ROOK); + MoveEffect corner4 = MoveEffect::place(H8, BLACK_ROOK); + + corner1.apply(board); + corner2.apply(board); + corner3.apply(board); + corner4.apply(board); + + EXPECT_EQ(board.get_piece(A1), WHITE_ROOK); + EXPECT_EQ(board.get_piece(H1), WHITE_ROOK); + EXPECT_EQ(board.get_piece(A8), BLACK_ROOK); + EXPECT_EQ(board.get_piece(H8), BLACK_ROOK); +} + +/** + * @test Both colors of same piece type. + * @brief Confirms effects work for both white and black pieces. + */ +TEST(MoveEffectTest, BothColorsOfSamePieceType) { + Board board = Board::Empty(); + + MoveEffect white_pawn = MoveEffect::place(E4, WHITE_PAWN); + MoveEffect black_pawn = MoveEffect::place(E5, BLACK_PAWN); + + white_pawn.apply(board); + black_pawn.apply(board); + + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); + EXPECT_EQ(board.get_piece(E5), BLACK_PAWN); +} + +/** + * @test Revert place effect doesn't affect other pieces. + * @brief Confirms reverting place effect only affects the specific square. + */ +TEST(MoveEffectTest, RevertPlaceDoesNotAffectOtherPieces) { + Board board = Board::Empty(); + board.set_piece(D4, WHITE_KNIGHT); + board.set_piece(F4, WHITE_BISHOP); + + MoveEffect effect = MoveEffect::place(E4, WHITE_PAWN); + + effect.apply(board); + effect.revert(board); + + EXPECT_EQ(board.get_piece(D4), WHITE_KNIGHT); + EXPECT_EQ(board.get_piece(F4), WHITE_BISHOP); + EXPECT_EQ(board.get_piece(E4), std::nullopt); +} + +/** + * @test Revert remove effect doesn't affect other pieces. + * @brief Confirms reverting remove effect only affects the specific square. + */ +TEST(MoveEffectTest, RevertRemoveDoesNotAffectOtherPieces) { + Board board = Board::Empty(); + board.set_piece(E4, WHITE_PAWN); + board.set_piece(D4, WHITE_KNIGHT); + board.set_piece(F4, WHITE_BISHOP); + + MoveEffect effect = MoveEffect::remove(E4, WHITE_PAWN); + + effect.apply(board); + effect.revert(board); + + EXPECT_EQ(board.get_piece(E4), WHITE_PAWN); + EXPECT_EQ(board.get_piece(D4), WHITE_KNIGHT); + EXPECT_EQ(board.get_piece(F4), WHITE_BISHOP); +} diff --git a/tests/bitbishop/moves/test_move_execution.cpp b/tests/bitbishop/moves/test_move_execution.cpp new file mode 100644 index 0000000..f4e0830 --- /dev/null +++ b/tests/bitbishop/moves/test_move_execution.cpp @@ -0,0 +1,115 @@ +#include + +#include +#include +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +TEST(MoveExecutionTest, ApplyPlaceEffect) { + Board board; + MoveExecution exec; + + exec.add(MoveEffect::place(E4, WHITE_PAWN)); + exec.apply(board); + + auto piece = board.get_piece(E4); + ASSERT_TRUE(piece.has_value()); + EXPECT_EQ(piece.value(), WHITE_PAWN); + + exec.revert(board); + EXPECT_FALSE(board.get_piece(E4).has_value()); +} + +TEST(MoveExecutionTest, ApplyRemoveEffect) { + Board board; + MoveExecution exec; + + // Place a piece manually (outside MoveExecution) + board.set_piece(E4, WHITE_PAWN); + + exec.add(MoveEffect::remove(E4, WHITE_PAWN)); + exec.apply(board); + + // Board should now have empty square + EXPECT_FALSE(board.get_piece(E4).has_value()); + + exec.revert(board); + + // Board should restore pawn + auto piece = board.get_piece(E4); + ASSERT_TRUE(piece.has_value()); + EXPECT_EQ(piece.value(), WHITE_PAWN); +} + +TEST(MoveExecutionTest, RevertHappensInReverseOrder) { + Board board; + MoveExecution exec; + + exec.add(MoveEffect::place(A1, WHITE_ROOK)); + exec.add(MoveEffect::place(B1, WHITE_KNIGHT)); + exec.add(MoveEffect::place(C1, WHITE_BISHOP)); + + exec.apply(board); + + EXPECT_TRUE(board.get_piece(A1)); + EXPECT_TRUE(board.get_piece(B1)); + EXPECT_TRUE(board.get_piece(C1)); + + exec.revert(board); + + EXPECT_FALSE(board.get_piece(A1)); + EXPECT_FALSE(board.get_piece(B1)); + EXPECT_FALSE(board.get_piece(C1)); +} + +TEST(MoveExecutionTest, ApplyAndRevertBoardState) { + Board board; + MoveExecution exec; + + BoardState prev = board.get_state(); + BoardState next = prev; + next.m_is_white_turn = !prev.m_is_white_turn; + + exec.add(MoveEffect::state_change(prev, next)); + exec.apply(board); + + EXPECT_EQ(board.get_state(), next); + + exec.revert(board); + + EXPECT_EQ(board.get_state(), prev); +} + +TEST(MoveExecutionTest, ApplyAndRevertZeroEffects) { + Board board; + MoveExecution exec; + + Board copy = board; + + exec.apply(board); + exec.revert(board); + + EXPECT_EQ(board, copy); +} + +TEST(MoveExecutionTest, DoubleApplyDoubleRevertRestoresBoard) { + Board board; + Board original = board; + + MoveExecution exec; + exec.add(MoveEffect::place(C3, WHITE_BISHOP)); + + exec.apply(board); + exec.apply(board); + + ASSERT_TRUE(board.get_piece(C3).has_value()); + + exec.revert(board); + exec.revert(board); + + EXPECT_EQ(board, original); +} diff --git a/tests/bitbishop/moves/test_position.cpp b/tests/bitbishop/moves/test_position.cpp new file mode 100644 index 0000000..2d506d6 --- /dev/null +++ b/tests/bitbishop/moves/test_position.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include +#include +#include + +using namespace Squares; +using namespace Pieces; + +TEST(PositionTest, ApplyMoveUpdatesBoard) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + Position pos(std::move(std::move(board))); + + Move move = Move::make(E2, E4, false); + + pos.apply_move(move); + + // The pawn should now be on E4 + EXPECT_EQ(pos.get_board().get_piece(E4), WHITE_PAWN); + + // The original square should be empty + EXPECT_FALSE(pos.get_board().get_piece(E2).has_value()); +} + +TEST(PositionTest, RevertMoveRestoresBoard) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + Position pos(std::move(board)); + + Move move = Move::make(E2, E4, false); + + pos.apply_move(move); + + // Apply move changed board + EXPECT_EQ(pos.get_board().get_piece(E4), WHITE_PAWN); + EXPECT_FALSE(pos.get_board().get_piece(E2).has_value()); + + pos.revert_move(); + + // Revert should restore original board + EXPECT_EQ(pos.get_board().get_piece(E2), WHITE_PAWN); + EXPECT_FALSE(pos.get_board().get_piece(E4).has_value()); +} + +TEST(PositionTest, CanUnmakeReflectsMoveHistory) { + Board board = Board::Empty(); + board.set_piece(E2, WHITE_PAWN); + + Position pos(std::move(board)); + + // No moves yet + EXPECT_FALSE(pos.can_unmake()); + + Move move = Move::make(E2, E4, false); + pos.apply_move(move); + + // After one move, can_unmake should be true + EXPECT_TRUE(pos.can_unmake()); + + pos.revert_move(); + + // After revert, history is empty + EXPECT_FALSE(pos.can_unmake()); +}