From 8738870a7b0e220f1ee58c015a089a987286742d Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Wed, 7 Jan 2026 00:17:35 +0100 Subject: [PATCH 01/12] added make and unmake move functions - wip --- include/bitbishop/board.hpp | 24 ++++++ src/bitbishop/board.cpp | 163 ++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/include/bitbishop/board.hpp b/include/bitbishop/board.hpp index c10795e..9177098 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -2,9 +2,27 @@ #include #include +#include #include #include #include +#include + +struct UndoInfo { + std::optional captured_piece; + Square captured_square; + + bool white_castle_kingside; + bool white_castle_queenside; + bool black_castle_kingside; + bool black_castle_queenside; + + std::optional en_passant_sq; + int halfmove_clock; + int fullmove_number; + + bool was_pawn_promotion; // true if a pawn was promoted +}; /** * @class Board @@ -46,6 +64,8 @@ class Board { // Move number (starts at 1, incremented after Black’s move) int m_fullmove_number; + std::vector m_undo_stack; + public: /** * @brief Constructs an empty starting board. @@ -282,6 +302,10 @@ class Board { */ [[nodiscard]] bool can_castle_queenside(Color side) const noexcept; + void make_move(const Move& move); + + void unmake_move(const Move& move); + Board& operator=(const Board& other) = default; /** diff --git a/src/bitbishop/board.cpp b/src/bitbishop/board.cpp index 0976b52..e6b2dc8 100644 --- a/src/bitbishop/board.cpp +++ b/src/bitbishop/board.cpp @@ -269,6 +269,169 @@ bool Board::can_castle_queenside(Color side) const noexcept { return !occupied.test(b_sq) && !occupied.test(c_sq) && !occupied.test(d_sq); } +void Board::make_move(const Move& move) { + using namespace Squares; + using namespace Const; + + Piece moving_piece = *get_piece(move.from); + bool is_white = moving_piece.is_white(); + bool is_pawn_move = moving_piece.type() == Piece::Type::PAWN; + + // 1. Update halfmove clock BEFORE altering board state + if (is_pawn_move || move.is_capture || move.is_en_passant) { + m_halfmove_clock = 0; + } else { + m_halfmove_clock++; + } + + // 2. En passant capture (remove pawn behind destination) + if (move.is_en_passant) { + int direction = is_white ? -BOARD_WIDTH : +BOARD_WIDTH; + Square captured = Square(move.to.value() + direction); + remove_piece(captured); + } + + // 3. Move the piece + move_piece(move.from, move.to); + + // 4. Promotion + if (move.promotion) { + set_piece(move.to, *move.promotion); + } + + // 5. Castling rook movement + if (move.is_castling) { + // clang-format off + if (move.to == C1) {move_piece(A1, D1);} + if (move.to == G1) {move_piece(H1, F1);} + if (move.to == C8) {move_piece(A8, D8);} + if (move.to == G8) {move_piece(H8, F8);} + // clang-format on + } + + // 6. Reset en passant square + m_en_passant_sq = std::nullopt; + + // 7. Double pawn push sets en passant square + if (is_pawn_move && std::abs(move.to.rank() - move.from.rank()) == 2) { + int ep_rank = (move.to.rank() + move.from.rank()) / 2; + m_en_passant_sq = Square(move.from.file(), ep_rank); + } + + // 8. Update castling rights + // king moved + if (moving_piece.type() == Piece::Type::KING) { + if (is_white) { + m_white_castle_kingside = false; + m_white_castle_queenside = false; + } else { + m_black_castle_kingside = false; + m_black_castle_queenside = false; + } + } + + // rook moved + if (moving_piece.type() == Piece::Type::ROOK) { + // clang-format off + if (move.from == A1) {m_white_castle_queenside = false;} + if (move.from == H1) {m_white_castle_kingside = false;} + if (move.from == A8) {m_black_castle_queenside = false;} + if (move.from == H8) {m_black_castle_kingside = false;} + // clang-format on + } + + // rook captured + if (move.is_capture || move.is_en_passant) { + // clang-format off + if (move.to == A1) {m_white_castle_queenside = false;} + if (move.to == H1) {m_white_castle_kingside = false;} + if (move.to == A8) {m_black_castle_queenside = false;} + if (move.to == H8) {m_black_castle_kingside = false;} + // clang-format on + } + + // 9. Flip side to move + m_is_white_turn = !m_is_white_turn; + + // 10. Fullmove counter + if (!m_is_white_turn) { + m_fullmove_number++; + } + + // 11. Save move information for undo + UndoInfo undo = { + .captured_piece = get_piece(move.to), + .captured_square = move.to, + .white_castle_kingside = m_white_castle_kingside, + .white_castle_queenside = m_white_castle_queenside, + .black_castle_kingside = m_black_castle_kingside, + .black_castle_queenside = m_black_castle_queenside, + .en_passant_sq = m_en_passant_sq, + .halfmove_clock = m_halfmove_clock, + .fullmove_number = m_fullmove_number, + .was_pawn_promotion = move.promotion.has_value(), + }; + m_undo_stack.push_back(undo); +} + +void Board::unmake_move(const Move& move) { + UndoInfo undo = m_undo_stack.back(); + m_undo_stack.pop_back(); + + // 1. Flip side to move back + m_is_white_turn = !m_is_white_turn; + + // 2. Restore halfmove clock and fullmove number + m_halfmove_clock = undo.halfmove_clock; + m_fullmove_number = undo.fullmove_number; + + // 3. Restore en passant square + m_en_passant_sq = undo.en_passant_sq; + + // 4. Move piece back + Piece moving_piece = *get_piece(move.to); + + // Undo promotion if any + if (undo.was_pawn_promotion) { + moving_piece = Piece(Piece::Type::PAWN, moving_piece.color()); + } + + move_piece(move.to, move.from); + set_piece(move.from, moving_piece); + + // 5. Restore captured piece if any + if (undo.captured_piece) { + set_piece(undo.captured_square, undo.captured_piece.value()); + } + + // 6. Undo castling rook moves + if (move.is_castling) { + using namespace Squares; + // clang-format off + if (move.to == C1) {move_piece(D1, A1);} + if (move.to == G1) {move_piece(F1, H1);} + if (move.to == C8) {move_piece(D8, A8);} + if (move.to == G8) {move_piece(F8, H8);} + // clang-format on + } + + // 7. Restore castling rights + m_white_castle_kingside = undo.white_castle_kingside; + m_white_castle_queenside = undo.white_castle_queenside; + m_black_castle_kingside = undo.black_castle_kingside; + m_black_castle_queenside = undo.black_castle_queenside; + + // 8. Undo en passant capture if applicable + if (move.is_en_passant) { + using namespace Const; + int direction = m_is_white_turn ? BOARD_WIDTH : -BOARD_WIDTH; + Square captured = Square(move.to.value() - direction); + set_piece(captured, *undo.captured_piece); + remove_piece(move.to); // Remove the capturing pawn temporarily moved + move_piece(move.from, move.from); // fix positions if necessary + } +} + bool Board::operator==(const Board& other) const { if (this == &other) { return true; From 6a57dd5d2be4d718e3705054bdea076b99347df5 Mon Sep 17 00:00:00 2001 From: Baptiste Penot Date: Thu, 8 Jan 2026 01:08:08 +0100 Subject: [PATCH 02/12] save --- include/bitbishop/board.hpp | 347 ++++++++++++++++++++++++++++++++---- src/bitbishop/board.cpp | 192 +++----------------- 2 files changed, 340 insertions(+), 199 deletions(-) diff --git a/include/bitbishop/board.hpp b/include/bitbishop/board.hpp index 9177098..9d09ccd 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -8,22 +8,25 @@ #include #include -struct UndoInfo { - std::optional captured_piece; - Square captured_square; +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 - bool white_castle_kingside; - bool white_castle_queenside; - bool black_castle_kingside; - bool black_castle_queenside; + // 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 - std::optional en_passant_sq; - int halfmove_clock; - int fullmove_number; + // 50-move rule state + int m_halfmove_clock; ///< Counts halfmoves since last pawn move or capture - bool was_pawn_promotion; // true if a pawn was promoted + // Move number (starts at 1, incremented after Black’s move) + int m_fullmove_number; }; +struct MoveExecution; + /** * @class Board * @brief Represents a complete chess position. @@ -49,22 +52,8 @@ 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; - - std::vector m_undo_stack; + BoardState m_state; + std::vector m_move_history; public: /** @@ -241,6 +230,9 @@ class Board { */ [[nodiscard]] Bitboard friendly(Color side) const { return (side == Color::WHITE) ? white_pieces() : black_pieces(); } + 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. * @@ -254,7 +246,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. @@ -262,7 +254,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; } /** @@ -271,7 +263,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; } /** @@ -302,8 +294,24 @@ class Board { */ [[nodiscard]] bool can_castle_queenside(Color side) const noexcept; + // void Board::make_move(const Move& move) { + // MoveExecution exec = MoveExecutor::build_execution(*this, move); + // exec.apply(*this); + // m_move_history.push_back(exec); + // } + // not relevant in board void make_move(const Move& move); + // void Board::unmake_move(const Move& move) { + // if (m_move_history.empty()) { + // throw std::runtime_error("No moves to unmake"); + // } + + // MoveExecution exec = m_move_history.back(); + // m_move_history.pop_back(); + // exec.revert(*this); + // } + // not relevant in board void unmake_move(const Move& move); Board& operator=(const Board& other) = default; @@ -329,3 +337,282 @@ class Board { */ [[nodiscard]] bool operator!=(const Board& other) const; }; + +struct MoveEffect { + enum class Type : uint8_t { Place, Remove, BoardState, Dummy }; + + Type type; + Square square; + Piece piece; + BoardState prev_state; + BoardState new_state; + + MoveEffect(Type t, Square sq, Piece p, BoardState prev = BoardState{}, BoardState next = BoardState{}) + : type(t), square(sq), piece(p), prev_state(prev), new_state(next) {} + + MoveEffect() : type(Type::Dummy), square(Squares::A1), piece(Pieces::WHITE_KING), prev_state(), new_state() { ; } + + static MoveEffect place(Square sq, Piece p) { return MoveEffect(Type::Place, sq, p); } + + static MoveEffect remove(Square sq, Piece p) { return MoveEffect(Type::Remove, sq, p); } + + static MoveEffect state_change(BoardState prev, BoardState next) { + return MoveEffect(Type::BoardState, Squares::A1, Pieces::WHITE_KING, prev, next); + } + + inline void 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(new_state); + break; + case Type::Dummy: + break; + } + } + + inline void 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; + case Type::Dummy: + break; + } + } +}; + +struct MoveExecution { + static constexpr int MAX_EFFECTS = 6; + MoveEffect effects[MAX_EFFECTS]; + int count = 0; + + inline void add(const MoveEffect& e) { effects[count++] = e; } + + inline void apply(Board& board) const { + for (int i = 0; i < count; ++i) { + effects[i].apply(board); + } + } + + inline void revert(Board& board) const { + for (int i = count - 1; i >= 0; --i) { + effects[i].revert(board); + } + } +}; + +struct MoveContext { + const Board& board; + const Move& move; + Piece moving_piece; + bool is_white; + bool is_pawn_move; + BoardState prev_state; +}; + +// Rule function signature: takes context, generates effects into execution +using MoveRule = void (*)(const MoveContext& ctx, MoveExecution& exec); + +namespace MoveRules { + +inline void basic_movement(const MoveContext& ctx, MoveExecution& exec) { + if (ctx.move.is_capture && !ctx.move.is_en_passant) { + Piece captured = *ctx.board.get_piece(ctx.move.to); + exec.add(MoveEffect::remove(ctx.move.to, captured)); + } + + exec.add(MoveEffect::remove(ctx.move.from, ctx.moving_piece)); + exec.add(MoveEffect::place(ctx.move.to, ctx.moving_piece)); +} + +inline void en_passant_capture(const MoveContext& ctx, MoveExecution& exec) { + if (!ctx.move.is_en_passant) { + return; + } + + using namespace Const; + int direction = ctx.is_white ? -BOARD_WIDTH : +BOARD_WIDTH; + Square captured = Square(ctx.move.to.value() + direction); + Piece captured_piece = *ctx.board.get_piece(captured); + exec.add(MoveEffect::remove(captured, captured_piece)); +} + +inline void pawn_promotion(const MoveContext& ctx, MoveExecution& exec) { + if (!ctx.move.promotion) { + return; + } + + exec.add(MoveEffect::remove(ctx.move.to, ctx.moving_piece)); + exec.add(MoveEffect::place(ctx.move.to, *ctx.move.promotion)); +} + +inline void castling_rook(const MoveContext& ctx, MoveExecution& exec) { + if (!ctx.move.is_castling) { + return; + } + + using namespace Squares; + + // dummy inits + Square rook_from = Squares::A1; + Square rook_to = Squares::A1; + + if (ctx.move.to == C1) { + rook_from = A1; + rook_to = D1; + } else if (ctx.move.to == G1) { + rook_from = H1; + rook_to = F1; + } else if (ctx.move.to == C8) { + rook_from = A8; + rook_to = D8; + } else if (ctx.move.to == G8) { + rook_from = H8; + rook_to = F8; + } else { + return; + } + + Piece rook = *ctx.board.get_piece(rook_from); + exec.add(MoveEffect::remove(rook_from, rook)); + exec.add(MoveEffect::place(rook_to, rook)); +} + +} // namespace MoveRules + +class BoardStateBuilder { + private: + BoardState state; + + public: + explicit BoardStateBuilder(const BoardState& prev) : state(prev) {} + + // Halfmove clock + void reset_halfmove_clock() { state.m_halfmove_clock = 0; } + void increment_halfmove_clock() { state.m_halfmove_clock++; } + + // En passant + void clear_en_passant() { state.m_en_passant_sq = std::nullopt; } + void set_en_passant(Square sq) { state.m_en_passant_sq = sq; } + + // Castling rights + void revoke_white_castling() { + state.m_white_castle_kingside = false; + state.m_white_castle_queenside = false; + } + + void revoke_black_castling() { + state.m_black_castle_kingside = false; + state.m_black_castle_queenside = false; + } + + void revoke_castling_if_rook_at(Square sq) { + using namespace Squares; + if (sq == A1) state.m_white_castle_queenside = false; + if (sq == H1) state.m_white_castle_kingside = false; + if (sq == A8) state.m_black_castle_queenside = false; + if (sq == H8) state.m_black_castle_kingside = false; + } + + // Turn + void flip_turn() { state.m_is_white_turn = !state.m_is_white_turn; } + + // Fullmove + void increment_fullmove_if_black_moved() { + if (state.m_is_white_turn) { // White is about to move = black just moved + state.m_fullmove_number++; + } + } + + BoardState build() const { return state; } +}; + +namespace StateRules { + +// Single function that builds the new state +inline void update_board_state(const MoveContext& ctx, MoveExecution& exec) { + BoardStateBuilder builder(ctx.prev_state); + + // Halfmove clock + if (ctx.is_pawn_move || ctx.move.is_capture || ctx.move.is_en_passant) { + builder.reset_halfmove_clock(); + } else { + builder.increment_halfmove_clock(); + } + + // En passant + builder.clear_en_passant(); + if (ctx.is_pawn_move && std::abs(ctx.move.to.rank() - ctx.move.from.rank()) == 2) { + int ep_rank = (ctx.move.to.rank() + ctx.move.from.rank()) / 2; + builder.set_en_passant(Square(ctx.move.from.file(), ep_rank)); + } + + // Castling rights + if (ctx.moving_piece.type() == Piece::Type::KING) { + if (ctx.is_white) { + builder.revoke_white_castling(); + } else { + builder.revoke_black_castling(); + } + } + + if (ctx.moving_piece.type() == Piece::Type::ROOK) { + builder.revoke_castling_if_rook_at(ctx.move.from); + } + + if (ctx.move.is_capture || ctx.move.is_en_passant) { + builder.revoke_castling_if_rook_at(ctx.move.to); + } + + // Turn and fullmove + builder.flip_turn(); + builder.increment_fullmove_if_black_moved(); + + // Add the state change effect + exec.add(MoveEffect::state_change(ctx.prev_state, builder.build())); +} + +} // namespace StateRules + +class MoveExecutor { + public: + static MoveExecution build_execution(const Board& board, const Move& move) { + const MoveRule STANDARD_RULES[5] = { + // clang-format off + MoveRules::en_passant_capture, + MoveRules::basic_movement, + MoveRules::pawn_promotion, + MoveRules::castling_rook, + StateRules::update_board_state, // Single state update at end + // clang-format on + }; + + MoveContext ctx = { + .board = board, + .move = move, + .moving_piece = *board.get_piece(move.from), + .is_white = board.get_piece(move.from)->is_white(), + .is_pawn_move = board.get_piece(move.from)->type() == Piece::Type::PAWN, + .prev_state = board.get_state(), + }; + + MoveExecution exec{}; + for (MoveRule rule : STANDARD_RULES) { + rule(ctx, exec); + } + + return exec; + } +}; diff --git a/src/bitbishop/board.cpp b/src/bitbishop/board.cpp index e6b2dc8..71709d7 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 { @@ -270,166 +270,19 @@ bool Board::can_castle_queenside(Color side) const noexcept { } void Board::make_move(const Move& move) { - using namespace Squares; - using namespace Const; - - Piece moving_piece = *get_piece(move.from); - bool is_white = moving_piece.is_white(); - bool is_pawn_move = moving_piece.type() == Piece::Type::PAWN; - - // 1. Update halfmove clock BEFORE altering board state - if (is_pawn_move || move.is_capture || move.is_en_passant) { - m_halfmove_clock = 0; - } else { - m_halfmove_clock++; - } - - // 2. En passant capture (remove pawn behind destination) - if (move.is_en_passant) { - int direction = is_white ? -BOARD_WIDTH : +BOARD_WIDTH; - Square captured = Square(move.to.value() + direction); - remove_piece(captured); - } - - // 3. Move the piece - move_piece(move.from, move.to); - - // 4. Promotion - if (move.promotion) { - set_piece(move.to, *move.promotion); - } - - // 5. Castling rook movement - if (move.is_castling) { - // clang-format off - if (move.to == C1) {move_piece(A1, D1);} - if (move.to == G1) {move_piece(H1, F1);} - if (move.to == C8) {move_piece(A8, D8);} - if (move.to == G8) {move_piece(H8, F8);} - // clang-format on - } - - // 6. Reset en passant square - m_en_passant_sq = std::nullopt; - - // 7. Double pawn push sets en passant square - if (is_pawn_move && std::abs(move.to.rank() - move.from.rank()) == 2) { - int ep_rank = (move.to.rank() + move.from.rank()) / 2; - m_en_passant_sq = Square(move.from.file(), ep_rank); - } - - // 8. Update castling rights - // king moved - if (moving_piece.type() == Piece::Type::KING) { - if (is_white) { - m_white_castle_kingside = false; - m_white_castle_queenside = false; - } else { - m_black_castle_kingside = false; - m_black_castle_queenside = false; - } - } - - // rook moved - if (moving_piece.type() == Piece::Type::ROOK) { - // clang-format off - if (move.from == A1) {m_white_castle_queenside = false;} - if (move.from == H1) {m_white_castle_kingside = false;} - if (move.from == A8) {m_black_castle_queenside = false;} - if (move.from == H8) {m_black_castle_kingside = false;} - // clang-format on - } - - // rook captured - if (move.is_capture || move.is_en_passant) { - // clang-format off - if (move.to == A1) {m_white_castle_queenside = false;} - if (move.to == H1) {m_white_castle_kingside = false;} - if (move.to == A8) {m_black_castle_queenside = false;} - if (move.to == H8) {m_black_castle_kingside = false;} - // clang-format on - } - - // 9. Flip side to move - m_is_white_turn = !m_is_white_turn; - - // 10. Fullmove counter - if (!m_is_white_turn) { - m_fullmove_number++; - } - - // 11. Save move information for undo - UndoInfo undo = { - .captured_piece = get_piece(move.to), - .captured_square = move.to, - .white_castle_kingside = m_white_castle_kingside, - .white_castle_queenside = m_white_castle_queenside, - .black_castle_kingside = m_black_castle_kingside, - .black_castle_queenside = m_black_castle_queenside, - .en_passant_sq = m_en_passant_sq, - .halfmove_clock = m_halfmove_clock, - .fullmove_number = m_fullmove_number, - .was_pawn_promotion = move.promotion.has_value(), - }; - m_undo_stack.push_back(undo); + MoveExecution exec = MoveExecutor::build_execution(*this, move); + exec.apply(*this); + m_move_history.push_back(exec); } void Board::unmake_move(const Move& move) { - UndoInfo undo = m_undo_stack.back(); - m_undo_stack.pop_back(); - - // 1. Flip side to move back - m_is_white_turn = !m_is_white_turn; - - // 2. Restore halfmove clock and fullmove number - m_halfmove_clock = undo.halfmove_clock; - m_fullmove_number = undo.fullmove_number; - - // 3. Restore en passant square - m_en_passant_sq = undo.en_passant_sq; - - // 4. Move piece back - Piece moving_piece = *get_piece(move.to); - - // Undo promotion if any - if (undo.was_pawn_promotion) { - moving_piece = Piece(Piece::Type::PAWN, moving_piece.color()); + if (m_move_history.empty()) { + throw std::runtime_error("No moves to unmake"); } - move_piece(move.to, move.from); - set_piece(move.from, moving_piece); - - // 5. Restore captured piece if any - if (undo.captured_piece) { - set_piece(undo.captured_square, undo.captured_piece.value()); - } - - // 6. Undo castling rook moves - if (move.is_castling) { - using namespace Squares; - // clang-format off - if (move.to == C1) {move_piece(D1, A1);} - if (move.to == G1) {move_piece(F1, H1);} - if (move.to == C8) {move_piece(D8, A8);} - if (move.to == G8) {move_piece(F8, H8);} - // clang-format on - } - - // 7. Restore castling rights - m_white_castle_kingside = undo.white_castle_kingside; - m_white_castle_queenside = undo.white_castle_queenside; - m_black_castle_kingside = undo.black_castle_kingside; - m_black_castle_queenside = undo.black_castle_queenside; - - // 8. Undo en passant capture if applicable - if (move.is_en_passant) { - using namespace Const; - int direction = m_is_white_turn ? BOARD_WIDTH : -BOARD_WIDTH; - Square captured = Square(move.to.value() - direction); - set_piece(captured, *undo.captured_piece); - remove_piece(move.to); // Remove the capturing pawn temporarily moved - move_piece(move.from, move.from); // fix positions if necessary - } + MoveExecution exec = m_move_history.back(); + m_move_history.pop_back(); + exec.revert(*this); } bool Board::operator==(const Board& other) const { @@ -443,11 +296,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); } From 4d53a8736bd2350e2fa627fbd882c0f372df38ab Mon Sep 17 00:00:00 2001 From: baptiste Date: Thu, 8 Jan 2026 23:29:17 +0100 Subject: [PATCH 03/12] trying another more promising move effects implementation --- include/bitbishop/board.hpp | 300 --------------------------- include/bitbishop/move_builder.hpp | 36 ++++ include/bitbishop/move_effect.hpp | 22 ++ include/bitbishop/move_execution.hpp | 15 ++ include/bitbishop/position.hpp | 22 ++ src/bitbishop/move_builder.cpp | 112 ++++++++++ src/bitbishop/move_effect.cpp | 50 +++++ src/bitbishop/move_execution.cpp | 15 ++ 8 files changed, 272 insertions(+), 300 deletions(-) create mode 100644 include/bitbishop/move_builder.hpp create mode 100644 include/bitbishop/move_effect.hpp create mode 100644 include/bitbishop/move_execution.hpp create mode 100644 include/bitbishop/position.hpp create mode 100644 src/bitbishop/move_builder.cpp create mode 100644 src/bitbishop/move_effect.cpp create mode 100644 src/bitbishop/move_execution.cpp diff --git a/include/bitbishop/board.hpp b/include/bitbishop/board.hpp index 9d09ccd..8ac68bc 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -53,7 +53,6 @@ class Board { // Game state BoardState m_state; - std::vector m_move_history; public: /** @@ -294,26 +293,6 @@ class Board { */ [[nodiscard]] bool can_castle_queenside(Color side) const noexcept; - // void Board::make_move(const Move& move) { - // MoveExecution exec = MoveExecutor::build_execution(*this, move); - // exec.apply(*this); - // m_move_history.push_back(exec); - // } - // not relevant in board - void make_move(const Move& move); - - // void Board::unmake_move(const Move& move) { - // if (m_move_history.empty()) { - // throw std::runtime_error("No moves to unmake"); - // } - - // MoveExecution exec = m_move_history.back(); - // m_move_history.pop_back(); - // exec.revert(*this); - // } - // not relevant in board - void unmake_move(const Move& move); - Board& operator=(const Board& other) = default; /** @@ -337,282 +316,3 @@ class Board { */ [[nodiscard]] bool operator!=(const Board& other) const; }; - -struct MoveEffect { - enum class Type : uint8_t { Place, Remove, BoardState, Dummy }; - - Type type; - Square square; - Piece piece; - BoardState prev_state; - BoardState new_state; - - MoveEffect(Type t, Square sq, Piece p, BoardState prev = BoardState{}, BoardState next = BoardState{}) - : type(t), square(sq), piece(p), prev_state(prev), new_state(next) {} - - MoveEffect() : type(Type::Dummy), square(Squares::A1), piece(Pieces::WHITE_KING), prev_state(), new_state() { ; } - - static MoveEffect place(Square sq, Piece p) { return MoveEffect(Type::Place, sq, p); } - - static MoveEffect remove(Square sq, Piece p) { return MoveEffect(Type::Remove, sq, p); } - - static MoveEffect state_change(BoardState prev, BoardState next) { - return MoveEffect(Type::BoardState, Squares::A1, Pieces::WHITE_KING, prev, next); - } - - inline void 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(new_state); - break; - case Type::Dummy: - break; - } - } - - inline void 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; - case Type::Dummy: - break; - } - } -}; - -struct MoveExecution { - static constexpr int MAX_EFFECTS = 6; - MoveEffect effects[MAX_EFFECTS]; - int count = 0; - - inline void add(const MoveEffect& e) { effects[count++] = e; } - - inline void apply(Board& board) const { - for (int i = 0; i < count; ++i) { - effects[i].apply(board); - } - } - - inline void revert(Board& board) const { - for (int i = count - 1; i >= 0; --i) { - effects[i].revert(board); - } - } -}; - -struct MoveContext { - const Board& board; - const Move& move; - Piece moving_piece; - bool is_white; - bool is_pawn_move; - BoardState prev_state; -}; - -// Rule function signature: takes context, generates effects into execution -using MoveRule = void (*)(const MoveContext& ctx, MoveExecution& exec); - -namespace MoveRules { - -inline void basic_movement(const MoveContext& ctx, MoveExecution& exec) { - if (ctx.move.is_capture && !ctx.move.is_en_passant) { - Piece captured = *ctx.board.get_piece(ctx.move.to); - exec.add(MoveEffect::remove(ctx.move.to, captured)); - } - - exec.add(MoveEffect::remove(ctx.move.from, ctx.moving_piece)); - exec.add(MoveEffect::place(ctx.move.to, ctx.moving_piece)); -} - -inline void en_passant_capture(const MoveContext& ctx, MoveExecution& exec) { - if (!ctx.move.is_en_passant) { - return; - } - - using namespace Const; - int direction = ctx.is_white ? -BOARD_WIDTH : +BOARD_WIDTH; - Square captured = Square(ctx.move.to.value() + direction); - Piece captured_piece = *ctx.board.get_piece(captured); - exec.add(MoveEffect::remove(captured, captured_piece)); -} - -inline void pawn_promotion(const MoveContext& ctx, MoveExecution& exec) { - if (!ctx.move.promotion) { - return; - } - - exec.add(MoveEffect::remove(ctx.move.to, ctx.moving_piece)); - exec.add(MoveEffect::place(ctx.move.to, *ctx.move.promotion)); -} - -inline void castling_rook(const MoveContext& ctx, MoveExecution& exec) { - if (!ctx.move.is_castling) { - return; - } - - using namespace Squares; - - // dummy inits - Square rook_from = Squares::A1; - Square rook_to = Squares::A1; - - if (ctx.move.to == C1) { - rook_from = A1; - rook_to = D1; - } else if (ctx.move.to == G1) { - rook_from = H1; - rook_to = F1; - } else if (ctx.move.to == C8) { - rook_from = A8; - rook_to = D8; - } else if (ctx.move.to == G8) { - rook_from = H8; - rook_to = F8; - } else { - return; - } - - Piece rook = *ctx.board.get_piece(rook_from); - exec.add(MoveEffect::remove(rook_from, rook)); - exec.add(MoveEffect::place(rook_to, rook)); -} - -} // namespace MoveRules - -class BoardStateBuilder { - private: - BoardState state; - - public: - explicit BoardStateBuilder(const BoardState& prev) : state(prev) {} - - // Halfmove clock - void reset_halfmove_clock() { state.m_halfmove_clock = 0; } - void increment_halfmove_clock() { state.m_halfmove_clock++; } - - // En passant - void clear_en_passant() { state.m_en_passant_sq = std::nullopt; } - void set_en_passant(Square sq) { state.m_en_passant_sq = sq; } - - // Castling rights - void revoke_white_castling() { - state.m_white_castle_kingside = false; - state.m_white_castle_queenside = false; - } - - void revoke_black_castling() { - state.m_black_castle_kingside = false; - state.m_black_castle_queenside = false; - } - - void revoke_castling_if_rook_at(Square sq) { - using namespace Squares; - if (sq == A1) state.m_white_castle_queenside = false; - if (sq == H1) state.m_white_castle_kingside = false; - if (sq == A8) state.m_black_castle_queenside = false; - if (sq == H8) state.m_black_castle_kingside = false; - } - - // Turn - void flip_turn() { state.m_is_white_turn = !state.m_is_white_turn; } - - // Fullmove - void increment_fullmove_if_black_moved() { - if (state.m_is_white_turn) { // White is about to move = black just moved - state.m_fullmove_number++; - } - } - - BoardState build() const { return state; } -}; - -namespace StateRules { - -// Single function that builds the new state -inline void update_board_state(const MoveContext& ctx, MoveExecution& exec) { - BoardStateBuilder builder(ctx.prev_state); - - // Halfmove clock - if (ctx.is_pawn_move || ctx.move.is_capture || ctx.move.is_en_passant) { - builder.reset_halfmove_clock(); - } else { - builder.increment_halfmove_clock(); - } - - // En passant - builder.clear_en_passant(); - if (ctx.is_pawn_move && std::abs(ctx.move.to.rank() - ctx.move.from.rank()) == 2) { - int ep_rank = (ctx.move.to.rank() + ctx.move.from.rank()) / 2; - builder.set_en_passant(Square(ctx.move.from.file(), ep_rank)); - } - - // Castling rights - if (ctx.moving_piece.type() == Piece::Type::KING) { - if (ctx.is_white) { - builder.revoke_white_castling(); - } else { - builder.revoke_black_castling(); - } - } - - if (ctx.moving_piece.type() == Piece::Type::ROOK) { - builder.revoke_castling_if_rook_at(ctx.move.from); - } - - if (ctx.move.is_capture || ctx.move.is_en_passant) { - builder.revoke_castling_if_rook_at(ctx.move.to); - } - - // Turn and fullmove - builder.flip_turn(); - builder.increment_fullmove_if_black_moved(); - - // Add the state change effect - exec.add(MoveEffect::state_change(ctx.prev_state, builder.build())); -} - -} // namespace StateRules - -class MoveExecutor { - public: - static MoveExecution build_execution(const Board& board, const Move& move) { - const MoveRule STANDARD_RULES[5] = { - // clang-format off - MoveRules::en_passant_capture, - MoveRules::basic_movement, - MoveRules::pawn_promotion, - MoveRules::castling_rook, - StateRules::update_board_state, // Single state update at end - // clang-format on - }; - - MoveContext ctx = { - .board = board, - .move = move, - .moving_piece = *board.get_piece(move.from), - .is_white = board.get_piece(move.from)->is_white(), - .is_pawn_move = board.get_piece(move.from)->type() == Piece::Type::PAWN, - .prev_state = board.get_state(), - }; - - MoveExecution exec{}; - for (MoveRule rule : STANDARD_RULES) { - rule(ctx, exec); - } - - return exec; - } -}; diff --git a/include/bitbishop/move_builder.hpp b/include/bitbishop/move_builder.hpp new file mode 100644 index 0000000..f347b65 --- /dev/null +++ b/include/bitbishop/move_builder.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +class MoveBuilder { + private: + MoveExecution m_effects; + + Move m_move; + Board m_board; + + Piece m_final_piece; + Piece m_moving_piece; + std::optional m_captured; + BoardState m_prev_state, m_next_state; + + public: + MoveBuilder() = delete; + MoveBuilder(const Board& board, const Move& move); + + MoveExecution build() const; + + void remove_moving_piece(); + void regular_capture(); + void en_passant_capture(); + void handle_promotion(); + void place_final_piece(); + void handle_rook_castling(); + void update_castling_rights(); + void add_en_passant_square(); + void commit_state(); + void update_half_move_clock(); + void update_full_move_number(); + void flip_side_to_move(); + void reset_en_passant_square(); +}; diff --git a/include/bitbishop/move_effect.hpp b/include/bitbishop/move_effect.hpp new file mode 100644 index 0000000..ca6d1a4 --- /dev/null +++ b/include/bitbishop/move_effect.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +struct MoveEffect { + enum class Type : uint8_t { Place, Remove, BoardState }; + + Type type; + Square square; + Piece piece; + BoardState prev_state; + BoardState next_state; + + static MoveEffect place(Square sq, Piece p); + static MoveEffect remove(Square sq, Piece p); + static MoveEffect state_change(const BoardState& prev, const BoardState& next); + + void apply(Board& board) const; + void revert(Board& board) const; +}; diff --git a/include/bitbishop/move_execution.hpp b/include/bitbishop/move_execution.hpp new file mode 100644 index 0000000..9b430e7 --- /dev/null +++ b/include/bitbishop/move_execution.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +struct MoveExecution { + static constexpr int MAX_EFFECTS = 6; + + std::array effects; + int count = 0; + + void add(const MoveEffect& effect); + void apply(Board& board) const; + void revert(Board& board) const; +}; diff --git a/include/bitbishop/position.hpp b/include/bitbishop/position.hpp new file mode 100644 index 0000000..47de202 --- /dev/null +++ b/include/bitbishop/position.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +class Position { + private: + Board m_board; + std::vector m_history; + + public: + Position() {} + + explicit Position(const Board& initial) {} + + void apply_move(const Move& move) {} + void revert_move() {} + + const Board& get_board() const {} + Board& board() {} + + bool can_unmake() const {} +}; diff --git a/src/bitbishop/move_builder.cpp b/src/bitbishop/move_builder.cpp new file mode 100644 index 0000000..d942f90 --- /dev/null +++ b/src/bitbishop/move_builder.cpp @@ -0,0 +1,112 @@ +#include +#include + +MoveBuilder::MoveBuilder(const Board& board, const Move& move) : m_board(board), m_move(move) { + m_moving_piece = *board.get_piece(move.from); + m_final_piece = m_moving_piece; + m_captured = board.get_piece(move.to); + + m_prev_state = board.get_state(); + m_next_state = board.get_state(); +} + +void MoveBuilder::remove_moving_piece() { m_effects.add(MoveEffect::remove(m_move.from, m_moving_piece)); } + +void MoveBuilder::regular_capture() { + if (m_captured && !m_move.is_en_passant) { + m_effects.add(MoveEffect::place(m_move.to, *m_captured)); + } +} + +void MoveBuilder::en_passant_capture() { + if (m_move.is_en_passant) { + ; // todo + } +} + +void MoveBuilder::handle_promotion() { + if (m_move.promotion) { + m_final_piece = *m_move.promotion; + } +} + +void MoveBuilder::place_final_piece() { m_effects.add(MoveEffect::place(m_move.to, m_final_piece)); } + +void MoveBuilder::handle_rook_castling() { + using namespace Const; + + if (m_move.is_castling) { + const bool is_kingside = m_move.to.value() > m_move.from.value(); + const int from_rank = m_move.from.rank(); + Color color = (m_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); + + m_effects.add(MoveEffect::remove(rook_from, rook_piece)); + m_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); + + m_effects.add(MoveEffect::remove(rook_from, rook_piece)); + m_effects.add(MoveEffect::place(rook_to, rook_piece)); + } + } +} + +void MoveBuilder::update_castling_rights() { + const bool is_king_moving = m_moving_piece.type() == Piece::Type::KING; + if (is_king_moving) { + if (m_prev_state.m_is_white_turn) { + m_next_state.m_white_castle_kingside = false; + m_next_state.m_white_castle_queenside = false; + } else { + m_next_state.m_black_castle_kingside = false; + m_next_state.m_black_castle_queenside = false; + } + } + + const bool is_rook_moving = m_moving_piece.type() == Piece::Type::ROOK; + const bool is_rook_captured = m_captured ? m_captured.type() == Piece::Type::ROOK : false; + if (is_rook_moving || is_rook_captured) { + // todo + } +} + +void MoveBuilder::add_en_passant_square() { + const bool is_pawn_moving = m_moving_piece.type() == Piece::Type::PAWN; + + if (is_pawn_moving) { + const int dr = m_move.to.rank() - m_move.from.rank(); + if (dr == 2) { + m_next_state.m_en_passant_sq = Square(m_move.from.file(), m_move.from.rank() - 1); + } else if (dr == -2) { + m_next_state.m_en_passant_sq = Square(m_move.from.file(), m_move.from.rank() + 1); + } + } +} + +void MoveBuilder::commit_state() { effects.add(MoveEffect::state_change(m_prev_state, m_next_state)); } + +void MoveBuilder::update_half_move_clock() { + const bool is_pawn_moving = m_moving_piece.type() == Piece::Type::PAWN; // make it common? + + if (m_move.is_capture || is_pawn_moving) { + m_next_state.m_halfmove_clock = 0; + } else { + m_next_state.m_halfmove_clock++; + } +} + +void MoveBuilder::update_full_move_number() { + if (!m_prev_state.m_is_white_turn) { + m_next_state.m_fullmove_number++; + } +} + +void MoveBuilder::flip_side_to_move() { m_next_state.m_is_white_turn = !m_prev_state.m_is_white_turn; } + +void MoveBuilder::reset_en_passant_square() { m_next_state.m_en_passant_sq = std::nullopt; } \ No newline at end of file diff --git a/src/bitbishop/move_effect.cpp b/src/bitbishop/move_effect.cpp new file mode 100644 index 0000000..c1373cc --- /dev/null +++ b/src/bitbishop/move_effect.cpp @@ -0,0 +1,50 @@ +#include +#include + +MoveEffect MoveEffect::place(Square sq, Piece p) { + return MoveEffect{.type = Type::Place, .square = sq, .piece = p, .prev_state = {}, .next_state = {}}; +} + +MoveEffect MoveEffect::remove(Square sq, Piece p) { + return MoveEffect{.type = Type::Remove, .square = sq, .piece = p, .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/move_execution.cpp b/src/bitbishop/move_execution.cpp new file mode 100644 index 0000000..3015354 --- /dev/null +++ b/src/bitbishop/move_execution.cpp @@ -0,0 +1,15 @@ +#include + +void MoveExecution::add(const MoveEffect& effect) { effects[count++] = effect; } + +void MoveExecution::apply(Board& board) const { + for (uint8_t i = 0; i < count; ++i) { + effects[i].apply(board); + } +} + +void MoveExecution::revert(Board& board) const { + for (uint8_t i = count - 1; i >= 0; --i) { + effects[i].revert(board); + } +} From ec7364600928da610a1f9c3934ec2d6e472f1733 Mon Sep 17 00:00:00 2001 From: baptiste Date: Fri, 9 Jan 2026 23:52:57 +0100 Subject: [PATCH 04/12] it compiles and it's not too bad --- include/bitbishop/move_builder.hpp | 32 +++-- include/bitbishop/move_effect.hpp | 4 +- include/bitbishop/position.hpp | 21 ++-- src/bitbishop/board.cpp | 16 --- src/bitbishop/move_builder.cpp | 191 +++++++++++++++++++---------- src/bitbishop/position.cpp | 17 +++ 6 files changed, 177 insertions(+), 104 deletions(-) create mode 100644 src/bitbishop/position.cpp diff --git a/include/bitbishop/move_builder.hpp b/include/bitbishop/move_builder.hpp index f347b65..d50ff2c 100644 --- a/include/bitbishop/move_builder.hpp +++ b/include/bitbishop/move_builder.hpp @@ -4,30 +4,38 @@ class MoveBuilder { private: - MoveExecution m_effects; + MoveExecution effects; - Move m_move; - Board m_board; + const Move& move; + const Board& board; - Piece m_final_piece; - Piece m_moving_piece; - std::optional m_captured; - BoardState m_prev_state, m_next_state; + Piece final_piece = Pieces::WHITE_KING; // place holder + Piece moving_piece = Pieces::WHITE_KING; // place holder + std::optional opt_captured_piece; + BoardState prev_state, next_state; public: MoveBuilder() = delete; MoveBuilder(const Board& board, const Move& move); + MoveExecution build(); - MoveExecution build() const; + private: + // utilities + void revoke_castling_if_rook_at(Square sq); + void revoke_castling_if_king_at(Square sq); + + void prepare_base_state(); + void prepare_next_state(); + // move execution builder steps void remove_moving_piece(); - void regular_capture(); - void en_passant_capture(); - void handle_promotion(); void place_final_piece(); + void handle_regular_capture(); + void handle_en_passant_capture(); + void handle_promotion(); void handle_rook_castling(); void update_castling_rights(); - void add_en_passant_square(); + void update_en_passant_square(); void commit_state(); void update_half_move_clock(); void update_full_move_number(); diff --git a/include/bitbishop/move_effect.hpp b/include/bitbishop/move_effect.hpp index ca6d1a4..d3ceec7 100644 --- a/include/bitbishop/move_effect.hpp +++ b/include/bitbishop/move_effect.hpp @@ -8,8 +8,8 @@ struct MoveEffect { enum class Type : uint8_t { Place, Remove, BoardState }; Type type; - Square square; - Piece piece; + Square square = Squares::A1; // place holder + Piece piece = Pieces::WHITE_KING; // place holder BoardState prev_state; BoardState next_state; diff --git a/include/bitbishop/position.hpp b/include/bitbishop/position.hpp index 47de202..6f46421 100644 --- a/include/bitbishop/position.hpp +++ b/include/bitbishop/position.hpp @@ -1,22 +1,21 @@ #pragma once -#include +#include +#include class Position { private: - Board m_board; - std::vector m_history; + Board board; + std::vector move_execution_history; public: - Position() {} + Position() = delete; + explicit Position(Board initial) : board(std::move(initial)) { ; } - explicit Position(const Board& initial) {} + void apply_move(const Move& move); + void revert_move(); - void apply_move(const Move& move) {} - void revert_move() {} + const Board& get_board() const { return board; } - const Board& get_board() const {} - Board& board() {} - - bool can_unmake() const {} + bool can_unmake() const { return !move_execution_history.empty(); } }; diff --git a/src/bitbishop/board.cpp b/src/bitbishop/board.cpp index 71709d7..2c0cbdc 100644 --- a/src/bitbishop/board.cpp +++ b/src/bitbishop/board.cpp @@ -269,22 +269,6 @@ bool Board::can_castle_queenside(Color side) const noexcept { return !occupied.test(b_sq) && !occupied.test(c_sq) && !occupied.test(d_sq); } -void Board::make_move(const Move& move) { - MoveExecution exec = MoveExecutor::build_execution(*this, move); - exec.apply(*this); - m_move_history.push_back(exec); -} - -void Board::unmake_move(const Move& move) { - if (m_move_history.empty()) { - throw std::runtime_error("No moves to unmake"); - } - - MoveExecution exec = m_move_history.back(); - m_move_history.pop_back(); - exec.revert(*this); -} - bool Board::operator==(const Board& other) const { if (this == &other) { return true; diff --git a/src/bitbishop/move_builder.cpp b/src/bitbishop/move_builder.cpp index d942f90..a7b446f 100644 --- a/src/bitbishop/move_builder.cpp +++ b/src/bitbishop/move_builder.cpp @@ -1,112 +1,177 @@ +#include #include -#include -MoveBuilder::MoveBuilder(const Board& board, const Move& move) : m_board(board), m_move(move) { - m_moving_piece = *board.get_piece(move.from); - m_final_piece = m_moving_piece; - m_captured = board.get_piece(move.to); +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); - m_prev_state = board.get_state(); - m_next_state = board.get_state(); + 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::remove_moving_piece() { m_effects.add(MoveEffect::remove(m_move.from, m_moving_piece)); } +void MoveBuilder::prepare_base_state() { + flip_side_to_move(); + update_half_move_clock(); + update_full_move_number(); + reset_en_passant_square(); +} -void MoveBuilder::regular_capture() { - if (m_captured && !m_move.is_en_passant) { - m_effects.add(MoveEffect::place(m_move.to, *m_captured)); +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::en_passant_capture() { - if (m_move.is_en_passant) { - ; // todo +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 = Square(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 (m_move.promotion) { - m_final_piece = *m_move.promotion; + if (move.promotion) { + final_piece = *move.promotion; } } -void MoveBuilder::place_final_piece() { m_effects.add(MoveEffect::place(m_move.to, m_final_piece)); } +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 (m_move.is_castling) { - const bool is_kingside = m_move.to.value() > m_move.from.value(); - const int from_rank = m_move.from.rank(); - Color color = (m_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); - 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); - m_effects.add(MoveEffect::remove(rook_from, rook_piece)); - m_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; + } +} - m_effects.add(MoveEffect::remove(rook_from, rook_piece)); - m_effects.add(MoveEffect::place(rook_to, rook_piece)); - } +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() { - const bool is_king_moving = m_moving_piece.type() == Piece::Type::KING; + const bool is_king_moving = moving_piece.type() == Piece::Type::KING; if (is_king_moving) { - if (m_prev_state.m_is_white_turn) { - m_next_state.m_white_castle_kingside = false; - m_next_state.m_white_castle_queenside = false; - } else { - m_next_state.m_black_castle_kingside = false; - m_next_state.m_black_castle_queenside = false; - } + revoke_castling_if_king_at(move.from); } - const bool is_rook_moving = m_moving_piece.type() == Piece::Type::ROOK; - const bool is_rook_captured = m_captured ? m_captured.type() == Piece::Type::ROOK : false; - if (is_rook_moving || is_rook_captured) { - // todo + const bool is_rook_moving = moving_piece.type() == Piece::Type::ROOK; + if (is_rook_moving) { + revoke_castling_if_rook_at(move.from); + } + + const bool is_rook_captured = opt_captured_piece ? (*opt_captured_piece).type() == Piece::Type::ROOK : false; + if (is_rook_captured) { + revoke_castling_if_rook_at(move.to); } } -void MoveBuilder::add_en_passant_square() { - const bool is_pawn_moving = m_moving_piece.type() == Piece::Type::PAWN; +void MoveBuilder::update_en_passant_square() { + const bool is_pawn_moving = moving_piece.type() == Piece::Type::PAWN; + if (!is_pawn_moving) { + return; + } - if (is_pawn_moving) { - const int dr = m_move.to.rank() - m_move.from.rank(); - if (dr == 2) { - m_next_state.m_en_passant_sq = Square(m_move.from.file(), m_move.from.rank() - 1); - } else if (dr == -2) { - m_next_state.m_en_passant_sq = Square(m_move.from.file(), m_move.from.rank() + 1); - } + const int dr = move.to.rank() - move.from.rank(); + if (dr == 2) { + next_state.m_en_passant_sq = Square(move.from.file(), move.from.rank() + 1); + } else if (dr == -2) { + next_state.m_en_passant_sq = Square(move.from.file(), move.from.rank() - 1); } } -void MoveBuilder::commit_state() { effects.add(MoveEffect::state_change(m_prev_state, m_next_state)); } +void MoveBuilder::commit_state() { effects.add(MoveEffect::state_change(prev_state, next_state)); } void MoveBuilder::update_half_move_clock() { - const bool is_pawn_moving = m_moving_piece.type() == Piece::Type::PAWN; // make it common? + const bool is_pawn_moving = moving_piece.type() == Piece::Type::PAWN; - if (m_move.is_capture || is_pawn_moving) { - m_next_state.m_halfmove_clock = 0; + if (move.is_capture || is_pawn_moving) { + next_state.m_halfmove_clock = 0; } else { - m_next_state.m_halfmove_clock++; + next_state.m_halfmove_clock++; } } void MoveBuilder::update_full_move_number() { - if (!m_prev_state.m_is_white_turn) { - m_next_state.m_fullmove_number++; + if (!next_state.m_is_white_turn) { + next_state.m_fullmove_number++; } } -void MoveBuilder::flip_side_to_move() { m_next_state.m_is_white_turn = !m_prev_state.m_is_white_turn; } +void MoveBuilder::flip_side_to_move() { next_state.m_is_white_turn = !prev_state.m_is_white_turn; } -void MoveBuilder::reset_en_passant_square() { m_next_state.m_en_passant_sq = std::nullopt; } \ No newline at end of file +void MoveBuilder::reset_en_passant_square() { next_state.m_en_passant_sq = std::nullopt; } diff --git a/src/bitbishop/position.cpp b/src/bitbishop/position.cpp new file mode 100644 index 0000000..1b454c8 --- /dev/null +++ b/src/bitbishop/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(); + } +} From fc76a64f9c205c9fc6847ede19fb1ae28e9a8c9c Mon Sep 17 00:00:00 2001 From: baptiste Date: Sun, 11 Jan 2026 19:38:16 +0100 Subject: [PATCH 05/12] implemented move system with tests --- include/bitbishop/bitboard.hpp | 15 + include/bitbishop/board.hpp | 24 +- .../bitbishop/{ => moves}/move_builder.hpp | 2 +- include/bitbishop/{ => moves}/move_effect.hpp | 4 +- .../bitbishop/{ => moves}/move_execution.hpp | 2 +- include/bitbishop/moves/position.hpp | 23 + include/bitbishop/position.hpp | 21 - src/bitbishop/board.cpp | 76 ++ src/bitbishop/{ => moves}/move_builder.cpp | 2 +- src/bitbishop/{ => moves}/move_effect.cpp | 2 +- src/bitbishop/{ => moves}/move_execution.cpp | 6 +- src/bitbishop/{ => moves}/position.cpp | 4 +- tests/bitbishop/board/test_b_board_state.cpp | 328 +++++++++ tests/bitbishop/board/test_b_constructors.cpp | 34 +- tests/bitbishop/moves/test_move_builder.cpp | 668 ++++++++++++++++++ tests/bitbishop/moves/test_move_effect.cpp | 407 +++++++++++ tests/bitbishop/moves/test_move_execution.cpp | 115 +++ tests/bitbishop/moves/test_position.cpp | 68 ++ 18 files changed, 1746 insertions(+), 55 deletions(-) rename include/bitbishop/{ => moves}/move_builder.hpp (95%) rename include/bitbishop/{ => moves}/move_effect.hpp (83%) rename include/bitbishop/{ => moves}/move_execution.hpp (86%) create mode 100644 include/bitbishop/moves/position.hpp delete mode 100644 include/bitbishop/position.hpp rename src/bitbishop/{ => moves}/move_builder.cpp (99%) rename src/bitbishop/{ => moves}/move_effect.cpp (97%) rename src/bitbishop/{ => moves}/move_execution.cpp (66%) rename src/bitbishop/{ => moves}/position.cpp (81%) create mode 100644 tests/bitbishop/board/test_b_board_state.cpp create mode 100644 tests/bitbishop/moves/test_move_builder.cpp create mode 100644 tests/bitbishop/moves/test_move_effect.cpp create mode 100644 tests/bitbishop/moves/test_move_execution.cpp create mode 100644 tests/bitbishop/moves/test_position.cpp diff --git a/include/bitbishop/bitboard.hpp b/include/bitbishop/bitboard.hpp index 99da1cb..abfd18a 100644 --- a/include/bitbishop/bitboard.hpp +++ b/include/bitbishop/bitboard.hpp @@ -50,6 +50,9 @@ class Bitboard { /** @brief Constructs a bitboard from another bitboard by copy. */ constexpr Bitboard(const Bitboard& bitboard) : m_bb(bitboard.value()) {} + /** @brief Move-constructs a bitboard. */ + constexpr explicit Bitboard(Bitboard&& other) noexcept : m_bb(std::move(other.m_bb)) { ; } + /** @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,18 @@ class Bitboard { return *this; } constexpr operator bool() const noexcept { return m_bb != 0ULL; } + constexpr Bitboard& operator=(const Bitboard& other) noexcept { + if (this != &other) { + m_bb = other.m_bb; + } + return *this; + } + constexpr Bitboard& operator=(Bitboard&& other) noexcept { + if (this != &other) { + m_bb = std::move(other.m_bb); + } + return *this; + } /** * @brief Counts the number of set bits in the bitboard. diff --git a/include/bitbishop/board.hpp b/include/bitbishop/board.hpp index 8ac68bc..360f525 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -23,9 +23,21 @@ struct BoardState { // Move number (starts at 1, incremented after Black’s move) int m_fullmove_number; -}; -struct MoveExecution; + 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 @@ -64,7 +76,8 @@ class Board { */ Board(); - Board(const Board&) = default; + Board(const Board&) noexcept; + explicit Board(Board&& other) noexcept; /** * @brief Constructs a board from a FEN string. @@ -229,7 +242,7 @@ class Board { */ [[nodiscard]] Bitboard friendly(Color side) const { return (side == Color::WHITE) ? white_pieces() : black_pieces(); } - BoardState get_state() const { return m_state; } + [[nodiscard]] BoardState get_state() const { return m_state; } void set_state(BoardState state) { m_state = state; } /** @@ -293,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; + Board& operator=(const Board&& other) noexcept; /** * @brief Checks if two boards represent the same chess position. diff --git a/include/bitbishop/move_builder.hpp b/include/bitbishop/moves/move_builder.hpp similarity index 95% rename from include/bitbishop/move_builder.hpp rename to include/bitbishop/moves/move_builder.hpp index d50ff2c..42749fa 100644 --- a/include/bitbishop/move_builder.hpp +++ b/include/bitbishop/moves/move_builder.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include class MoveBuilder { private: diff --git a/include/bitbishop/move_effect.hpp b/include/bitbishop/moves/move_effect.hpp similarity index 83% rename from include/bitbishop/move_effect.hpp rename to include/bitbishop/moves/move_effect.hpp index d3ceec7..5e367e8 100644 --- a/include/bitbishop/move_effect.hpp +++ b/include/bitbishop/moves/move_effect.hpp @@ -13,8 +13,8 @@ struct MoveEffect { BoardState prev_state; BoardState next_state; - static MoveEffect place(Square sq, Piece p); - static MoveEffect remove(Square sq, Piece p); + static MoveEffect place(Square sq, Piece piece); + static MoveEffect remove(Square sq, Piece piece); static MoveEffect state_change(const BoardState& prev, const BoardState& next); void apply(Board& board) const; diff --git a/include/bitbishop/move_execution.hpp b/include/bitbishop/moves/move_execution.hpp similarity index 86% rename from include/bitbishop/move_execution.hpp rename to include/bitbishop/moves/move_execution.hpp index 9b430e7..1d06932 100644 --- a/include/bitbishop/move_execution.hpp +++ b/include/bitbishop/moves/move_execution.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include struct MoveExecution { static constexpr int MAX_EFFECTS = 6; diff --git a/include/bitbishop/moves/position.hpp b/include/bitbishop/moves/position.hpp new file mode 100644 index 0000000..9d1dcf2 --- /dev/null +++ b/include/bitbishop/moves/position.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +class Position { + private: + Board board; + std::vector move_execution_history; + + public: + Position() = delete; + Position(Board&) = delete; + explicit Position(Board&& initial) : board(std::move(initial)) { ; } + + void apply_move(const Move& move); + void revert_move(); + + [[nodiscard]] const Board& get_board() const { return board; } + + [[nodiscard]] bool can_unmake() const { return !move_execution_history.empty(); } +}; diff --git a/include/bitbishop/position.hpp b/include/bitbishop/position.hpp deleted file mode 100644 index 6f46421..0000000 --- a/include/bitbishop/position.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -class Position { - private: - Board board; - std::vector move_execution_history; - - public: - Position() = delete; - explicit Position(Board initial) : board(std::move(initial)) { ; } - - void apply_move(const Move& move); - void revert_move(); - - const Board& get_board() const { return board; } - - bool can_unmake() const { return !move_execution_history.empty(); } -}; diff --git a/src/bitbishop/board.cpp b/src/bitbishop/board.cpp index 2c0cbdc..3bd7422 100644 --- a/src/bitbishop/board.cpp +++ b/src/bitbishop/board.cpp @@ -5,6 +5,36 @@ Board::Board() : Board("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") {} +Board::Board(const Board& other) noexcept + : m_w_pawns(other.m_w_pawns), + m_w_rooks(other.m_w_rooks), + m_w_bishops(other.m_w_bishops), + m_w_knights(other.m_w_knights), + m_w_king(other.m_w_king), + m_w_queens(other.m_w_queens), + m_b_pawns(other.m_b_pawns), + m_b_rooks(other.m_b_rooks), + m_b_bishops(other.m_b_bishops), + m_b_knights(other.m_b_knights), + m_b_king(other.m_b_king), + m_b_queens(other.m_b_queens), + m_state(other.m_state) {} + +Board::Board(Board&& other) noexcept + : m_w_pawns(std::move(other.m_w_pawns)), + m_w_rooks(std::move(other.m_w_rooks)), + m_w_bishops(std::move(other.m_w_bishops)), + m_w_knights(std::move(other.m_w_knights)), + m_w_king(std::move(other.m_w_king)), + m_w_queens(std::move(other.m_w_queens)), + m_b_pawns(std::move(other.m_b_pawns)), + m_b_rooks(std::move(other.m_b_rooks)), + m_b_bishops(std::move(other.m_b_bishops)), + m_b_knights(std::move(other.m_b_knights)), + m_b_king(std::move(other.m_b_king)), + m_b_queens(std::move(other.m_b_queens)), + m_state(std::move(other.m_state)) {} + Board::Board(const std::string& fen) { /* * https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation @@ -289,3 +319,49 @@ bool Board::operator==(const Board& other) const { } bool Board::operator!=(const Board& other) const { return !this->operator==(other); } + +Board& Board::operator=(const Board& other) noexcept { + if (this != &other) { + m_w_pawns = other.m_w_pawns; + m_w_rooks = other.m_w_rooks; + m_w_bishops = other.m_w_bishops; + m_w_knights = other.m_w_knights; + m_w_king = other.m_w_king; + m_w_queens = other.m_w_queens; + + m_b_pawns = other.m_b_pawns; + m_b_rooks = other.m_b_rooks; + m_b_bishops = other.m_b_bishops; + m_b_knights = other.m_b_knights; + m_b_king = other.m_b_king; + m_b_queens = other.m_b_queens; + + m_state = other.m_state; + } + + return *this; +} + +Board& Board::operator=(const Board&& other) noexcept { + if (this != &other) { + // Move all bitboards + m_w_pawns = std::move(other.m_w_pawns); + m_w_rooks = std::move(other.m_w_rooks); + m_w_bishops = std::move(other.m_w_bishops); + m_w_knights = std::move(other.m_w_knights); + m_w_king = std::move(other.m_w_king); + m_w_queens = std::move(other.m_w_queens); + + m_b_pawns = std::move(other.m_b_pawns); + m_b_rooks = std::move(other.m_b_rooks); + m_b_bishops = std::move(other.m_b_bishops); + m_b_knights = std::move(other.m_b_knights); + m_b_king = std::move(other.m_b_king); + m_b_queens = std::move(other.m_b_queens); + + // Move the board state + m_state = std::move(other.m_state); + } + + return *this; +} diff --git a/src/bitbishop/move_builder.cpp b/src/bitbishop/moves/move_builder.cpp similarity index 99% rename from src/bitbishop/move_builder.cpp rename to src/bitbishop/moves/move_builder.cpp index a7b446f..2459a95 100644 --- a/src/bitbishop/move_builder.cpp +++ b/src/bitbishop/moves/move_builder.cpp @@ -1,5 +1,5 @@ #include -#include +#include MoveBuilder::MoveBuilder(const Board& board, const Move& move) : board(board), move(move) { moving_piece = *board.get_piece(move.from); diff --git a/src/bitbishop/move_effect.cpp b/src/bitbishop/moves/move_effect.cpp similarity index 97% rename from src/bitbishop/move_effect.cpp rename to src/bitbishop/moves/move_effect.cpp index c1373cc..716a880 100644 --- a/src/bitbishop/move_effect.cpp +++ b/src/bitbishop/moves/move_effect.cpp @@ -1,4 +1,4 @@ -#include +#include #include MoveEffect MoveEffect::place(Square sq, Piece p) { diff --git a/src/bitbishop/move_execution.cpp b/src/bitbishop/moves/move_execution.cpp similarity index 66% rename from src/bitbishop/move_execution.cpp rename to src/bitbishop/moves/move_execution.cpp index 3015354..8db3f6c 100644 --- a/src/bitbishop/move_execution.cpp +++ b/src/bitbishop/moves/move_execution.cpp @@ -1,15 +1,15 @@ -#include +#include void MoveExecution::add(const MoveEffect& effect) { effects[count++] = effect; } void MoveExecution::apply(Board& board) const { - for (uint8_t i = 0; i < count; ++i) { + for (int i = 0; i < count; ++i) { effects[i].apply(board); } } void MoveExecution::revert(Board& board) const { - for (uint8_t i = count - 1; i >= 0; --i) { + for (int i = count - 1; i >= 0; --i) { effects[i].revert(board); } } diff --git a/src/bitbishop/position.cpp b/src/bitbishop/moves/position.cpp similarity index 81% rename from src/bitbishop/position.cpp rename to src/bitbishop/moves/position.cpp index 1b454c8..0314c83 100644 --- a/src/bitbishop/position.cpp +++ b/src/bitbishop/moves/position.cpp @@ -1,5 +1,5 @@ -#include -#include +#include +#include void Position::apply_move(const Move& move) { MoveBuilder builder(board, move); 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/board/test_b_constructors.cpp b/tests/bitbishop/board/test_b_constructors.cpp index 112f1c0..3e8a04e 100644 --- a/tests/bitbishop/board/test_b_constructors.cpp +++ b/tests/bitbishop/board/test_b_constructors.cpp @@ -57,23 +57,18 @@ TEST(BoardTest, FENConstructor) { } /** - * @test Copy constructor preserves board state. - * @brief Confirms that copying a board with the copy constructor produces an identical board. + * @test Move constructor preserves board state. + * @brief Confirms that moving a board with the move constructor produces an identical board. */ -TEST(BoardCopyTest, CopyConstructor) { +TEST(BoardCopyTest, MoveConstructor) { Board original = Board::Empty(); - // Set up pieces and state original.set_piece(E1, WHITE_KING); original.set_piece(E8, BLACK_KING); original.set_piece(D4, WHITE_QUEEN); original.set_piece(A7, BLACK_PAWN); - // Set castling rights manually (assuming setters exist) - // original.set_white_castle_kingside(true); - // original.set_black_castle_queenside(true); - - Board copy(original); // Copy constructor + Board copy(std::move(original)); // Copy constructor // Piece positions EXPECT_EQ(copy.get_piece(E1), WHITE_KING); @@ -89,10 +84,10 @@ TEST(BoardCopyTest, CopyConstructor) { } /** - * @test Copy assignment preserves board state. - * @brief Confirms that copying a board via assignment produces an identical board. + * @test Move assignment preserves board state. + * @brief Confirms that moving a board via assignment produces an identical board. */ -TEST(BoardCopyTest, CopyAssignment) { +TEST(BoardCopyTest, MoveAssignment) { Board original = Board::Empty(); // Set up pieces and state @@ -102,7 +97,7 @@ TEST(BoardCopyTest, CopyAssignment) { original.set_piece(H7, BLACK_PAWN); Board copy = Board::Empty(); - copy = original; // Copy assignment + copy = std::move(original); // Copy assignment // Piece positions EXPECT_EQ(copy.get_piece(E1), WHITE_KING); @@ -118,17 +113,20 @@ TEST(BoardCopyTest, CopyAssignment) { } /** - * @test Copy produces independent boards. + * @test Move produces independent boards. * @brief Modifying the copy does not affect the original board. */ -TEST(BoardCopyTest, IndependenceAfterCopy) { +TEST(BoardCopyTest, IndependenceAfterMove) { Board original = Board::Empty(); original.set_piece(E1, WHITE_KING); - Board copy(original); + Board copy(std::move(original)); copy.set_piece(E2, WHITE_PAWN); - // Original should remain unchanged - EXPECT_EQ(original.get_piece(E2), std::nullopt); + // Test that copy has the new piece EXPECT_EQ(copy.get_piece(E2), WHITE_PAWN); + + // Original is in a valid state; we can safely assign new values + original.set_piece(D1, WHITE_QUEEN); + EXPECT_EQ(original.get_piece(D1), WHITE_QUEEN); } 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()); +} From 013c4d57d27d32a64c73141208f0f4e2877c6ebb Mon Sep 17 00:00:00 2001 From: baptiste Date: Sun, 11 Jan 2026 19:47:41 +0100 Subject: [PATCH 06/12] reverted tests on board, that was a mistake --- tests/bitbishop/board/test_b_constructors.cpp | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/bitbishop/board/test_b_constructors.cpp b/tests/bitbishop/board/test_b_constructors.cpp index 3e8a04e..112f1c0 100644 --- a/tests/bitbishop/board/test_b_constructors.cpp +++ b/tests/bitbishop/board/test_b_constructors.cpp @@ -57,18 +57,23 @@ TEST(BoardTest, FENConstructor) { } /** - * @test Move constructor preserves board state. - * @brief Confirms that moving a board with the move constructor produces an identical board. + * @test Copy constructor preserves board state. + * @brief Confirms that copying a board with the copy constructor produces an identical board. */ -TEST(BoardCopyTest, MoveConstructor) { +TEST(BoardCopyTest, CopyConstructor) { Board original = Board::Empty(); + // Set up pieces and state original.set_piece(E1, WHITE_KING); original.set_piece(E8, BLACK_KING); original.set_piece(D4, WHITE_QUEEN); original.set_piece(A7, BLACK_PAWN); - Board copy(std::move(original)); // Copy constructor + // Set castling rights manually (assuming setters exist) + // original.set_white_castle_kingside(true); + // original.set_black_castle_queenside(true); + + Board copy(original); // Copy constructor // Piece positions EXPECT_EQ(copy.get_piece(E1), WHITE_KING); @@ -84,10 +89,10 @@ TEST(BoardCopyTest, MoveConstructor) { } /** - * @test Move assignment preserves board state. - * @brief Confirms that moving a board via assignment produces an identical board. + * @test Copy assignment preserves board state. + * @brief Confirms that copying a board via assignment produces an identical board. */ -TEST(BoardCopyTest, MoveAssignment) { +TEST(BoardCopyTest, CopyAssignment) { Board original = Board::Empty(); // Set up pieces and state @@ -97,7 +102,7 @@ TEST(BoardCopyTest, MoveAssignment) { original.set_piece(H7, BLACK_PAWN); Board copy = Board::Empty(); - copy = std::move(original); // Copy assignment + copy = original; // Copy assignment // Piece positions EXPECT_EQ(copy.get_piece(E1), WHITE_KING); @@ -113,20 +118,17 @@ TEST(BoardCopyTest, MoveAssignment) { } /** - * @test Move produces independent boards. + * @test Copy produces independent boards. * @brief Modifying the copy does not affect the original board. */ -TEST(BoardCopyTest, IndependenceAfterMove) { +TEST(BoardCopyTest, IndependenceAfterCopy) { Board original = Board::Empty(); original.set_piece(E1, WHITE_KING); - Board copy(std::move(original)); + Board copy(original); copy.set_piece(E2, WHITE_PAWN); - // Test that copy has the new piece + // Original should remain unchanged + EXPECT_EQ(original.get_piece(E2), std::nullopt); EXPECT_EQ(copy.get_piece(E2), WHITE_PAWN); - - // Original is in a valid state; we can safely assign new values - original.set_piece(D1, WHITE_QUEEN); - EXPECT_EQ(original.get_piece(D1), WHITE_QUEEN); } From f41dcf7b8aeb866a79069c75e2267772dea76fb3 Mon Sep 17 00:00:00 2001 From: baptiste Date: Sun, 11 Jan 2026 23:05:56 +0100 Subject: [PATCH 07/12] fixed linting and added usefull methods to Piece object --- .clang-tidy | 2 +- include/bitbishop/bitboard.hpp | 18 +-- include/bitbishop/board.hpp | 8 +- include/bitbishop/moves/position.hpp | 4 +- include/bitbishop/piece.hpp | 203 ++++++++++++++++++++++----- src/bitbishop/board.cpp | 76 ---------- src/bitbishop/moves/move_builder.cpp | 23 ++- src/bitbishop/moves/move_effect.cpp | 8 +- 8 files changed, 191 insertions(+), 151 deletions(-) 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 abfd18a..11fc6ff 100644 --- a/include/bitbishop/bitboard.hpp +++ b/include/bitbishop/bitboard.hpp @@ -48,10 +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 : m_bb(std::move(other.m_bb)) { ; } + 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); } @@ -169,18 +169,8 @@ class Bitboard { return *this; } constexpr operator bool() const noexcept { return m_bb != 0ULL; } - constexpr Bitboard& operator=(const Bitboard& other) noexcept { - if (this != &other) { - m_bb = other.m_bb; - } - return *this; - } - constexpr Bitboard& operator=(Bitboard&& other) noexcept { - if (this != &other) { - m_bb = std::move(other.m_bb); - } - return *this; - } + 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 360f525..d2ea5e6 100644 --- a/include/bitbishop/board.hpp +++ b/include/bitbishop/board.hpp @@ -76,8 +76,8 @@ class Board { */ Board(); - Board(const Board&) noexcept; - explicit Board(Board&& other) noexcept; + Board(const Board&) noexcept = default; + explicit Board(Board&& other) noexcept = default; /** * @brief Constructs a board from a FEN string. @@ -306,8 +306,8 @@ class Board { */ [[nodiscard]] bool can_castle_queenside(Color side) const noexcept; - Board& operator=(const Board& other) noexcept; - Board& operator=(const Board&& other) noexcept; + 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/position.hpp b/include/bitbishop/moves/position.hpp index 9d1dcf2..cad2b64 100644 --- a/include/bitbishop/moves/position.hpp +++ b/include/bitbishop/moves/position.hpp @@ -12,7 +12,9 @@ class Position { public: Position() = delete; Position(Board&) = delete; - explicit Position(Board&& initial) : board(std::move(initial)) { ; } + explicit Position(Board&& initial) : board(initial) { + ; // board is trivially copyable, no move, just a copy behind the scenes + } void apply_move(const Move& move); void revert_move(); diff --git a/include/bitbishop/piece.hpp b/include/bitbishop/piece.hpp index 698c1e8..879d430 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,184 @@ 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 3bd7422..2c0cbdc 100644 --- a/src/bitbishop/board.cpp +++ b/src/bitbishop/board.cpp @@ -5,36 +5,6 @@ Board::Board() : Board("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") {} -Board::Board(const Board& other) noexcept - : m_w_pawns(other.m_w_pawns), - m_w_rooks(other.m_w_rooks), - m_w_bishops(other.m_w_bishops), - m_w_knights(other.m_w_knights), - m_w_king(other.m_w_king), - m_w_queens(other.m_w_queens), - m_b_pawns(other.m_b_pawns), - m_b_rooks(other.m_b_rooks), - m_b_bishops(other.m_b_bishops), - m_b_knights(other.m_b_knights), - m_b_king(other.m_b_king), - m_b_queens(other.m_b_queens), - m_state(other.m_state) {} - -Board::Board(Board&& other) noexcept - : m_w_pawns(std::move(other.m_w_pawns)), - m_w_rooks(std::move(other.m_w_rooks)), - m_w_bishops(std::move(other.m_w_bishops)), - m_w_knights(std::move(other.m_w_knights)), - m_w_king(std::move(other.m_w_king)), - m_w_queens(std::move(other.m_w_queens)), - m_b_pawns(std::move(other.m_b_pawns)), - m_b_rooks(std::move(other.m_b_rooks)), - m_b_bishops(std::move(other.m_b_bishops)), - m_b_knights(std::move(other.m_b_knights)), - m_b_king(std::move(other.m_b_king)), - m_b_queens(std::move(other.m_b_queens)), - m_state(std::move(other.m_state)) {} - Board::Board(const std::string& fen) { /* * https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation @@ -319,49 +289,3 @@ bool Board::operator==(const Board& other) const { } bool Board::operator!=(const Board& other) const { return !this->operator==(other); } - -Board& Board::operator=(const Board& other) noexcept { - if (this != &other) { - m_w_pawns = other.m_w_pawns; - m_w_rooks = other.m_w_rooks; - m_w_bishops = other.m_w_bishops; - m_w_knights = other.m_w_knights; - m_w_king = other.m_w_king; - m_w_queens = other.m_w_queens; - - m_b_pawns = other.m_b_pawns; - m_b_rooks = other.m_b_rooks; - m_b_bishops = other.m_b_bishops; - m_b_knights = other.m_b_knights; - m_b_king = other.m_b_king; - m_b_queens = other.m_b_queens; - - m_state = other.m_state; - } - - return *this; -} - -Board& Board::operator=(const Board&& other) noexcept { - if (this != &other) { - // Move all bitboards - m_w_pawns = std::move(other.m_w_pawns); - m_w_rooks = std::move(other.m_w_rooks); - m_w_bishops = std::move(other.m_w_bishops); - m_w_knights = std::move(other.m_w_knights); - m_w_king = std::move(other.m_w_king); - m_w_queens = std::move(other.m_w_queens); - - m_b_pawns = std::move(other.m_b_pawns); - m_b_rooks = std::move(other.m_b_rooks); - m_b_bishops = std::move(other.m_b_bishops); - m_b_knights = std::move(other.m_b_knights); - m_b_king = std::move(other.m_b_king); - m_b_queens = std::move(other.m_b_queens); - - // Move the board state - m_state = std::move(other.m_state); - } - - return *this; -} diff --git a/src/bitbishop/moves/move_builder.cpp b/src/bitbishop/moves/move_builder.cpp index 2459a95..6b1b836 100644 --- a/src/bitbishop/moves/move_builder.cpp +++ b/src/bitbishop/moves/move_builder.cpp @@ -54,7 +54,7 @@ void MoveBuilder::handle_en_passant_capture() { using namespace Const; int direction = prev_state.m_is_white_turn ? -BOARD_WIDTH : +BOARD_WIDTH; - Square en_passant_sq = Square(move.to.value() + direction); + 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)); } @@ -124,32 +124,29 @@ void MoveBuilder::revoke_castling_if_king_at(Square sq) { } void MoveBuilder::update_castling_rights() { - const bool is_king_moving = moving_piece.type() == Piece::Type::KING; - if (is_king_moving) { + if (moving_piece.is_king()) { revoke_castling_if_king_at(move.from); } - const bool is_rook_moving = moving_piece.type() == Piece::Type::ROOK; - if (is_rook_moving) { + if (moving_piece.is_rook()) { revoke_castling_if_rook_at(move.from); } - const bool is_rook_captured = opt_captured_piece ? (*opt_captured_piece).type() == Piece::Type::ROOK : false; + 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() { - const bool is_pawn_moving = moving_piece.type() == Piece::Type::PAWN; - if (!is_pawn_moving) { + if (!moving_piece.is_pawn()) { return; } - const int dr = move.to.rank() - move.from.rank(); - if (dr == 2) { + 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 (dr == -2) { + } else if (delta_rank == -2) { next_state.m_en_passant_sq = Square(move.from.file(), move.from.rank() - 1); } } @@ -157,9 +154,7 @@ void MoveBuilder::update_en_passant_square() { void MoveBuilder::commit_state() { effects.add(MoveEffect::state_change(prev_state, next_state)); } void MoveBuilder::update_half_move_clock() { - const bool is_pawn_moving = moving_piece.type() == Piece::Type::PAWN; - - if (move.is_capture || is_pawn_moving) { + if (move.is_capture || moving_piece.is_pawn()) { next_state.m_halfmove_clock = 0; } else { next_state.m_halfmove_clock++; diff --git a/src/bitbishop/moves/move_effect.cpp b/src/bitbishop/moves/move_effect.cpp index 716a880..aee0bf7 100644 --- a/src/bitbishop/moves/move_effect.cpp +++ b/src/bitbishop/moves/move_effect.cpp @@ -1,12 +1,12 @@ #include #include -MoveEffect MoveEffect::place(Square sq, Piece p) { - return MoveEffect{.type = Type::Place, .square = sq, .piece = p, .prev_state = {}, .next_state = {}}; +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 p) { - return MoveEffect{.type = Type::Remove, .square = sq, .piece = p, .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) { From d8154201bc4bd24cd0b3fc6429683cf60edf6ea1 Mon Sep 17 00:00:00 2001 From: baptiste Date: Sun, 11 Jan 2026 23:22:08 +0100 Subject: [PATCH 08/12] fixed linting in piece hpp --- include/bitbishop/piece.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/include/bitbishop/piece.hpp b/include/bitbishop/piece.hpp index 879d430..ed46e62 100644 --- a/include/bitbishop/piece.hpp +++ b/include/bitbishop/piece.hpp @@ -148,8 +148,12 @@ class Piece { * @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; + 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); } From fb177fc7436b5a81f8898721c5af0f82a75106a3 Mon Sep 17 00:00:00 2001 From: baptiste Date: Mon, 12 Jan 2026 22:27:13 +0100 Subject: [PATCH 09/12] position docstrings --- include/bitbishop/moves/position.hpp | 77 +++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/include/bitbishop/moves/position.hpp b/include/bitbishop/moves/position.hpp index cad2b64..3029a67 100644 --- a/include/bitbishop/moves/position.hpp +++ b/include/bitbishop/moves/position.hpp @@ -4,22 +4,95 @@ #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; - Position(Board&) = delete; + 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(); } }; From 95f7895c228bdffa4e6604fc7e239b343b84c83f Mon Sep 17 00:00:00 2001 From: baptiste Date: Mon, 12 Jan 2026 22:30:35 +0100 Subject: [PATCH 10/12] move effect docstrings --- include/bitbishop/moves/move_effect.hpp | 46 ++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/include/bitbishop/moves/move_effect.hpp b/include/bitbishop/moves/move_effect.hpp index 5e367e8..25a61f4 100644 --- a/include/bitbishop/moves/move_effect.hpp +++ b/include/bitbishop/moves/move_effect.hpp @@ -4,19 +4,55 @@ #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; - Square square = Squares::A1; // place holder - Piece piece = Pieces::WHITE_KING; // place holder - BoardState prev_state; - BoardState next_state; + 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; }; From 07ae2091eef3280aabaaae887cae7949bd4a572f Mon Sep 17 00:00:00 2001 From: baptiste Date: Mon, 12 Jan 2026 22:31:46 +0100 Subject: [PATCH 11/12] move execution docstrings --- include/bitbishop/moves/move_execution.hpp | 29 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/include/bitbishop/moves/move_execution.hpp b/include/bitbishop/moves/move_execution.hpp index 1d06932..11cd4d8 100644 --- a/include/bitbishop/moves/move_execution.hpp +++ b/include/bitbishop/moves/move_execution.hpp @@ -3,13 +3,36 @@ #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; + static constexpr int MAX_EFFECTS = 6; ///< Maximum number of effects per move - std::array effects; - int count = 0; + 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; }; From 78de83e96e5b5ef4fd98f3cd8b903473783acab4 Mon Sep 17 00:00:00 2001 From: baptiste Date: Mon, 12 Jan 2026 22:34:20 +0100 Subject: [PATCH 12/12] move builder docstrings --- include/bitbishop/moves/move_builder.hpp | 75 +++++++++++++++--------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/include/bitbishop/moves/move_builder.hpp b/include/bitbishop/moves/move_builder.hpp index 42749fa..83b45f5 100644 --- a/include/bitbishop/moves/move_builder.hpp +++ b/include/bitbishop/moves/move_builder.hpp @@ -2,43 +2,62 @@ #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; + MoveExecution effects; ///< Stores the sequence of effects for the move - const Move& move; - const Board& board; + 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; // place holder - Piece moving_piece = Pieces::WHITE_KING; // place holder - std::optional opt_captured_piece; - BoardState prev_state, next_state; + 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 - void revoke_castling_if_rook_at(Square sq); - void revoke_castling_if_king_at(Square sq); - - void prepare_base_state(); - void prepare_next_state(); - - // move execution builder steps - void remove_moving_piece(); - void place_final_piece(); - void handle_regular_capture(); - void handle_en_passant_capture(); - void handle_promotion(); - void handle_rook_castling(); - void update_castling_rights(); - void update_en_passant_square(); - void commit_state(); - void update_half_move_clock(); - void update_full_move_number(); - void flip_side_to_move(); - void reset_en_passant_square(); + // 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 };