From 1a80d7d8e9f10150cad29dc0a825975c04583b5d Mon Sep 17 00:00:00 2001 From: nook0110 Date: Sat, 3 Jan 2026 00:48:55 +0300 Subject: [PATCH 1/5] Init --- Chess/ExitCondition.h | 1 + Chess/Move.h | 180 +++++++++--------- Chess/MoveFactory.h | 32 ++-- Chess/MoveGenerator.cpp | 76 +++----- Chess/Perft.cpp | 7 +- Chess/Position.cpp | 352 +++++++++++++++-------------------- Chess/Position.h | 18 +- Chess/Quiescence.cpp | 32 ++-- Chess/Searcher.h | 4 +- Chess/StreamUtility.h | 66 +------ Chess/TranspositionTable.h | 11 +- Tests/CompactMoveTests.cpp | 145 +++++++++++++++ Tests/MoveGeneratorTests.cpp | 4 +- 13 files changed, 471 insertions(+), 457 deletions(-) create mode 100644 Tests/CompactMoveTests.cpp diff --git a/Chess/ExitCondition.h b/Chess/ExitCondition.h index 75c1a8e..3a68b3b 100644 --- a/Chess/ExitCondition.h +++ b/Chess/ExitCondition.h @@ -1,4 +1,5 @@ #pragma once +#include #include "Evaluation.h" #include "Searcher.h" namespace SimpleChessEngine { diff --git a/Chess/Move.h b/Chess/Move.h index cf0ccd7..ba5818c 100644 --- a/Chess/Move.h +++ b/Chess/Move.h @@ -1,106 +1,116 @@ #pragma once #include +#include #include -#include - #include "BitBoard.h" #include "Piece.h" -namespace SimpleChessEngine { -struct NullMove {}; - -struct DefaultMove { - bool operator==(const DefaultMove&) const = default; - - BitIndex from{}; - BitIndex to{}; - Piece captured_piece{}; -}; - -struct PawnPush { - bool operator==(const PawnPush&) const = default; - - BitIndex from{}; - BitIndex to{}; -}; - -struct DoublePush { - bool operator==(const DoublePush&) const = default; - BitIndex from{}; - BitIndex to{}; -}; - -struct EnCroissant { - BitIndex from{}; - BitIndex to{}; - - bool operator==(const EnCroissant&) const = default; -}; +namespace SimpleChessEngine { -struct Promotion : DefaultMove { - bool operator==(const Promotion& other) const { - return DefaultMove::operator==(other) && promoted_to == other.promoted_to; - } +struct NullMove {}; - Piece promoted_to{}; +enum class MoveType : std::uint16_t { + kNormal = 0, + kPromotion = 1 << 14, + kEnPassant = 2 << 14, + kCastling = 3 << 14 }; -struct Castling { - enum class CastlingSide : std::uint8_t { k00, k000 }; - - CastlingSide side; - - BitIndex king_from{}; - BitIndex rook_from{}; - - bool operator==(const Castling&) const = default; +class Move { +private: + static constexpr std::uint16_t kSquareMask = 0x3F; + static constexpr std::uint16_t kPromotionMask = 0x3; + static constexpr std::uint16_t kTypeMask = 0x3; + static constexpr std::uint8_t kFromShift = 6; + static constexpr std::uint8_t kPromotionShift = 12; + static constexpr std::uint8_t kTypeShift = 14; + static constexpr std::uint16_t kNullValue = 65; + static constexpr std::uint16_t kNoneValue = 0; + +public: + Move() = default; + constexpr explicit Move(std::uint16_t data) : data_(data) {} + constexpr Move(BitIndex from, BitIndex to) : data_((from << kFromShift) + to) {} + + template + static constexpr Move Make(BitIndex from, BitIndex to, Piece promotion_piece = Piece::kKnight) { + return Move(static_cast(T) + + ((static_cast(promotion_piece) - static_cast(Piece::kKnight)) << kPromotionShift) + + (from << kFromShift) + to); + } + + constexpr BitIndex From() const { + assert(IsValid()); + return static_cast((data_ >> kFromShift) & kSquareMask); + } + + constexpr BitIndex To() const { + assert(IsValid()); + return static_cast(data_ & kSquareMask); + } + + constexpr MoveType Type() const { + return static_cast(data_ & (kTypeMask << kTypeShift)); + } + + constexpr Piece PromotionPiece() const { + return static_cast(((data_ >> kPromotionShift) & kPromotionMask) + static_cast(Piece::kKnight)); + } + + constexpr bool IsValid() const { + return data_ != kNoneValue && data_ != kNullValue; + } + + constexpr bool IsPromotion() const { return Type() == MoveType::kPromotion; } + constexpr bool IsEnPassant() const { return Type() == MoveType::kEnPassant; } + constexpr bool IsCastling() const { return Type() == MoveType::kCastling; } + constexpr bool IsNormal() const { return Type() == MoveType::kNormal; } + + static constexpr Move Null() { return Move(kNullValue); } + static constexpr Move None() { return Move(kNoneValue); } + + constexpr bool operator==(const Move& other) const = default; + constexpr bool operator!=(const Move& other) const = default; + constexpr explicit operator bool() const { return data_ != 0; } + constexpr std::uint16_t Raw() const { return data_; } + + struct Hash { + std::size_t operator()(const Move& move) const { + return move.data_ * 6364136223846793005ULL + 1442695040888963407ULL; + } + }; + +private: + std::uint16_t data_; }; -using Move = std::variant; - -inline std::tuple GetMoveData(const PawnPush& move) { - return {move.from, move.to, Piece::kNone}; -} - -inline std::tuple GetMoveData( - const DoublePush& move) { - return {move.from, move.to, Piece::kNone}; -} - -inline std::tuple GetMoveData( - const EnCroissant& move) { - return {move.from, move.to, Piece::kPawn}; -} - -inline std::tuple GetMoveData( - const DefaultMove& move) { - return {move.from, move.to, move.captured_piece}; -} - -inline std::tuple GetMoveData(const Castling& move) { - return {move.king_from, 64, Piece::kNone}; -} - -inline std::tuple GetMoveData( - const Promotion& move) { - return {move.from, move.to, move.captured_piece}; -} - +// Legacy compatibility functions inline std::tuple GetMoveData(const Move& move) { - return std::visit( - [](const auto& unwrapped_move) { return GetMoveData(unwrapped_move); }, - move); + if (move.IsEnPassant()) { + return {move.From(), move.To(), Piece::kPawn}; + } + if (move.IsCastling()) { + return {move.From(), 64, Piece::kNone}; + } + return {move.From(), move.To(), Piece::kNone}; } inline bool IsQuiet(const Move& move) { - return !std::get(GetMoveData(move)); + return move.IsNormal() && !move.IsEnPassant(); } inline bool DoesReset(const Move& move) { - if (std::holds_alternative(move)) { - return !!std::get(move).captured_piece; - } - return !std::holds_alternative(move); + return !move.IsCastling(); } + +// Castling side enum for position management +struct Castling { + enum class CastlingSide : std::uint8_t { k00, k000 }; +}; + +static_assert(sizeof(Move) == 2, "Move must be exactly 2 bytes"); +static_assert(alignof(Move) == 2, "Move should be 2-byte aligned"); +static_assert(std::is_trivially_copyable_v, "Move must be trivially copyable"); +static_assert(std::is_standard_layout_v, "Move must have standard layout"); + } // namespace SimpleChessEngine diff --git a/Chess/MoveFactory.h b/Chess/MoveFactory.h index ecebe04..2a5b582 100644 --- a/Chess/MoveFactory.h +++ b/Chess/MoveFactory.h @@ -10,6 +10,7 @@ #include "Utility.h" namespace SimpleChessEngine { + struct MoveFactory { Move operator()(const Position &position, const std::string &move) const; @@ -25,12 +26,14 @@ struct MoveFactory { inline Move MoveFactory::operator()(const Position &position, const std::string &move) const { if (move == "O-O") { - return Castling{Castling::CastlingSide::k00, - position.GetKingSquare(position.GetSideToMove())}; + return Move::Make( + position.GetKingSquare(position.GetSideToMove()), + position.GetKingSquare(position.GetSideToMove()) + 2); } if (move == "O-O-O") { - return Castling{Castling::CastlingSide::k000, - position.GetKingSquare(position.GetSideToMove())}; + return Move::Make( + position.GetKingSquare(position.GetSideToMove()), + position.GetKingSquare(position.GetSideToMove()) - 2); } const auto [from, to] = ParseDefaultMove(move); @@ -39,34 +42,27 @@ inline Move MoveFactory::operator()(const Position &position, if (piece_to_move == Piece::kKing) { if (!IsAdjacent(from, to)) { - static std::unordered_map castling_file = { - {2, Castling::CastlingSide::k000}, {6, Castling::CastlingSide::k00}}; - static std::unordered_map rook_from_file = {{1, 0}, {6, 7}}; - - auto [king_file, king_rank] = GetCoordinates(to); - - return Castling{castling_file[king_file], from, - GetSquareIndex(rook_from_file[king_file], king_rank)}; + return Move::Make(from, to); } } + if (constexpr size_t kPromotionSize = 5; move.size() == kPromotionSize) { - return Promotion{{from, to, position.GetPieceAt(to)}, - kCharToPiece[move.back()].first}; + return Move::Make(from, to, kCharToPiece[move.back()].first); } if (piece_to_move != Piece::kPawn || position.GetPieceAt(to) != Piece::kNone) { - return DefaultMove{from, to, position.GetPieceAt(to)}; + return Move(from, to); } if (!IsAdjacent(from, to)) { - return DoublePush{from, to}; + return Move(from, to); } if (to == position.GetEnCroissantSquare()) { - return EnCroissant{from, to}; + return Move::Make(from, to); } - return PawnPush{from, to}; + return Move(from, to); } inline MoveFactory::ParsedMove MoveFactory::ParseDefaultMove( diff --git a/Chess/MoveGenerator.cpp b/Chess/MoveGenerator.cpp index 55e2cd4..5afdb45 100644 --- a/Chess/MoveGenerator.cpp +++ b/Chess/MoveGenerator.cpp @@ -35,7 +35,7 @@ MoveGenerator::~MoveGenerator() = default; const Move& move) { const auto us = position.GetSideToMove(); - if (std::holds_alternative(move)) { + if (move.IsEnPassant()) { const auto irreversible_data = position.GetIrreversibleData(); position.DoMove(move); const auto valid = !position.IsUnderCheck(us); @@ -43,22 +43,9 @@ MoveGenerator::~MoveGenerator() = default; return valid; } - BitIndex from{}; - BitIndex to{}; - std::visit( - [&from, &to](const MoveType& unwrapped_move) { - if constexpr (std::same_as, - DefaultMove> || - std::same_as, PawnPush> || - std::same_as, DoublePush> || - std::same_as, Promotion>) { - from = unwrapped_move.from; - to = unwrapped_move.to; - return; - } - assert(false); - }, - move); + const auto from = move.From(); + const auto to = move.To(); + return !position.GetIrreversibleData().blockers[static_cast(us)].Test( from) || Ray(position.GetKingSquare(us), from).Test(to); @@ -70,15 +57,16 @@ void MoveGenerator::GenerateCastling(Moves& moves, const Position& position) { } const auto side_to_move = position.GetSideToMove(); - const auto king_square = position.GetKingSquare(side_to_move); + const auto color_idx = static_cast(side_to_move); for (const auto castling_side : {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { if (position.CanCastle(castling_side)) { - const auto rook_square = - position.GetCastlingRookSquare(side_to_move, castling_side); - moves.emplace_back(Castling{castling_side, king_square, rook_square}); + const auto side_idx = static_cast(castling_side); + const auto king_to = kKingCastlingDestination[color_idx][side_idx]; + + moves.emplace_back(Move::Make(king_square, king_to)); } } } @@ -123,10 +111,8 @@ void MoveGenerator::GenerateMovesForPiece( push &= target; while (push.Any()) { const auto to = push.PopFirstBit(); - const auto from = Shift(to, opposite_direction); - - moves.emplace_back(PawnPush{from, to}); + moves.emplace_back(Move(from, to)); } auto double_push = Shift(double_push_pawns, direction) & valid_squares; @@ -134,10 +120,8 @@ void MoveGenerator::GenerateMovesForPiece( double_push &= target; while (double_push.Any()) { const auto to = double_push.PopFirstBit(); - const auto from = Shift(Shift(to, opposite_direction), opposite_direction); - - moves.emplace_back(DoublePush{from, to}); + moves.emplace_back(Move(from, to)); } static constexpr std::array cant_attack_files = {kFileBB[0], kFileBB[7]}; @@ -166,10 +150,8 @@ void MoveGenerator::GenerateMovesForPiece( while (attack_squares.Any()) { const auto to = attack_squares.PopFirstBit(); - const auto from = Shift(to, opposite_attacks[attack_direction]); - - moves.emplace_back(DefaultMove{from, to, position.GetPieceAt(to)}); + moves.emplace_back(Move(from, to)); } } @@ -181,8 +163,8 @@ void MoveGenerator::GenerateMovesForPiece( auto attack_to = attacks_to[attack_direction] & en_croissant_bitboard; if (attack_to.Any()) { const auto to = en_croissant_square.value(); - moves.emplace_back( - EnCroissant{Shift(to, opposite_attacks[attack_direction]), to}); + const auto from = Shift(to, opposite_attacks[attack_direction]); + moves.emplace_back(Move::Make(from, to)); } } } @@ -194,17 +176,12 @@ void MoveGenerator::GenerateMovesForPiece( while (promotion_push.Any()) { const auto to = promotion_push.PopFirstBit(); - const auto from = Shift(to, opposite_direction); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kQueen}); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kKnight}); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kRook}); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kBishop}); + moves.emplace_back(Move::Make(from, to, Piece::kQueen)); + moves.emplace_back(Move::Make(from, to, Piece::kKnight)); + moves.emplace_back(Move::Make(from, to, Piece::kRook)); + moves.emplace_back(Move::Make(from, to, Piece::kBishop)); } for (size_t attack_direction = 0; attack_direction < attacks.size(); @@ -216,17 +193,12 @@ void MoveGenerator::GenerateMovesForPiece( while (attack_squares.Any()) { const auto to = attack_squares.PopFirstBit(); - const auto from = Shift(to, opposite_attacks[attack_direction]); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kKnight}); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kBishop}); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kRook}); - moves.emplace_back( - Promotion{{from, to, position.GetPieceAt(to)}, Piece::kQueen}); + moves.emplace_back(Move::Make(from, to, Piece::kKnight)); + moves.emplace_back(Move::Make(from, to, Piece::kBishop)); + moves.emplace_back(Move::Make(from, to, Piece::kRook)); + moves.emplace_back(Move::Make(from, to, Piece::kQueen)); } } } @@ -379,9 +351,7 @@ void MoveGenerator::GenerateMovesFromSquare(Moves& moves, Position& position, while (valid_moves.Any()) { const auto to = valid_moves.PopFirstBit(); - - const auto move = DefaultMove{from, to, position.GetPieceAt(to)}; - moves.emplace_back(move); + moves.emplace_back(Move(from, to)); } } } // namespace SimpleChessEngine diff --git a/Chess/Perft.cpp b/Chess/Perft.cpp index bc7e6c6..5c95aa5 100644 --- a/Chess/Perft.cpp +++ b/Chess/Perft.cpp @@ -19,12 +19,7 @@ size_t Perft(std::ostream& o_stream, Position& position, const Depth depth) { for (const auto& move : moves) { if constexpr (print) { - std::visit( - [&o_stream](const auto& unwrapped_move) { - o_stream << unwrapped_move; - }, - move); - o_stream << ": "; + o_stream << move << ": "; } size_t cur_answer; diff --git a/Chess/Position.cpp b/Chess/Position.cpp index 7126e95..fe4330b 100644 --- a/Chess/Position.cpp +++ b/Chess/Position.cpp @@ -103,8 +103,93 @@ void Position::DoMove(const Move &move) { .to_ulong()]; } - std::visit([this](const auto &unwrapped_move) { DoMove(unwrapped_move); }, - move); + const auto from = move.From(); + const auto to = move.To(); + const auto us = side_to_move_; + const auto them = Flip(us); + + // Store captured piece for undo + irreversible_data_.captured_piece = board_[to]; + + if (move.IsEnPassant()) { + const auto capture_square = Shift(to, kPawnMoveDirection[static_cast(them)]); + irreversible_data_.captured_piece = Piece::kPawn; // En passant always captures a pawn + RemovePiece(capture_square, them); + MovePiece(from, to, us); + } else if (move.IsPromotion()) { + const auto promoted_to = move.PromotionPiece(); + const auto captured_piece = board_[to]; + RemovePiece(from, us); + if (!!captured_piece) RemovePiece(to, them); + PlacePiece(to, promoted_to, us); + + for (const auto castling_side : {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { + const auto their_rook = rook_positions_[static_cast(them)][static_cast(castling_side)]; + if (to == their_rook) { + irreversible_data_.castling_rights[static_cast(them)] &= + ~static_cast(kCastlingRightsForSide[static_cast(castling_side)]); + } + } + } else if (move.IsCastling()) { + irreversible_data_.captured_piece = Piece::kNone; // Castling never captures + // Determine castling side and rook position from move + const auto king_to = to; + const auto king_from = from; + + // Determine which side based on king destination + const auto color_idx = static_cast(us); + Castling::CastlingSide side; + BitIndex rook_from; + + if (king_to == kKingCastlingDestination[color_idx][0]) { + side = Castling::CastlingSide::k00; + rook_from = rook_positions_[color_idx][0]; + } else { + side = Castling::CastlingSide::k000; + rook_from = rook_positions_[color_idx][1]; + } + + const auto side_idx = static_cast(side); + RemovePiece(king_from, us); + RemovePiece(rook_from, us); + PlacePiece(kKingCastlingDestination[color_idx][side_idx], Piece::kKing, us); + PlacePiece(kRookCastlingDestination[color_idx][side_idx], Piece::kRook, us); + + king_position_[color_idx] = kKingCastlingDestination[color_idx][side_idx]; + irreversible_data_.castling_rights[color_idx] = 0; + } else { + // Normal move (including pawn pushes and double pushes) + const auto piece_to_move = board_[from]; + const auto captured_piece = board_[to]; + + // Check if it's a double pawn push + if (piece_to_move == Piece::kPawn && std::abs(from - to) == 16) { + const auto file = GetCoordinates(from).first; + hash_ ^= hasher_.en_croissant_hash[file]; + irreversible_data_.en_croissant_square = std::midpoint(from, to); + } + + if (!!captured_piece) RemovePiece(to, them); + MovePiece(from, to, us); + + if (piece_to_move == Piece::kKing) { + king_position_[static_cast(us)] = to; + irreversible_data_.castling_rights[static_cast(us)] = 0; + } + + for (auto castling_side : {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { + const auto our_rook = rook_positions_[static_cast(us)][static_cast(castling_side)]; + const auto their_rook = rook_positions_[static_cast(them)][static_cast(castling_side)]; + if (from == our_rook) { + irreversible_data_.castling_rights[static_cast(us)] &= + ~static_cast(kCastlingRightsForSide[static_cast(castling_side)]); + } + if (to == their_rook) { + irreversible_data_.castling_rights[static_cast(them)] &= + ~static_cast(kCastlingRightsForSide[static_cast(castling_side)]); + } + } + } for (const auto color : {Player::kWhite, Player::kBlack}) { hash_ ^= hasher_.cr_hash[static_cast( @@ -129,115 +214,6 @@ void SimpleChessEngine::Position::DoMove(NullMove) { history_stack_.Push(hash_, false); } -void Position::DoMove(const DefaultMove &move) { - const auto [from, to, captured_piece] = move; - - const auto us = side_to_move_; - const auto them = Flip(us); - - const auto piece_to_move = board_[from]; - assert(!!piece_to_move); - - if (!!captured_piece) RemovePiece(to, them); - MovePiece(from, to, us); - - if (piece_to_move == Piece::kKing) { - king_position_[static_cast(us)] = to; - irreversible_data_.castling_rights[static_cast(us)] = 0; - } - - for (auto castling_side : - {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { - const auto our_rook = rook_positions_[static_cast(us)] - [static_cast(castling_side)]; - const auto their_rook = rook_positions_[static_cast(them)] - [static_cast(castling_side)]; - if (from == our_rook) { - irreversible_data_.castling_rights[static_cast(us)] &= - ~static_cast( - kCastlingRightsForSide[static_cast(castling_side)]); - } - if (to == their_rook) { - irreversible_data_.castling_rights[static_cast(them)] &= - ~static_cast( - kCastlingRightsForSide[static_cast(castling_side)]); - } - } -} - -void Position::DoMove(const PawnPush &move) { - const auto [from, to] = move; - - const auto us = side_to_move_; - - MovePiece(from, to, us); -} - -void Position::DoMove(const DoublePush &move) { - const auto [from, to] = move; - const auto file = GetCoordinates(from).first; - - const auto us = side_to_move_; - - hash_ ^= hasher_.en_croissant_hash[file]; - irreversible_data_.en_croissant_square = std::midpoint(from, to); - - MovePiece(from, to, us); -} - -void Position::DoMove(const EnCroissant &move) { - const auto [from, to] = move; - - const auto us = side_to_move_; - const auto them = Flip(us); - - const auto capture_square = - Shift(to, kPawnMoveDirection[static_cast(them)]); - - RemovePiece(capture_square, them); - MovePiece(from, to, us); -} - -void Position::DoMove(const Promotion &move) { - const auto [from, to, captured_piece] = static_cast(move); - const auto promoted_to = move.promoted_to; - - const auto us = side_to_move_; - const auto them = Flip(us); - - RemovePiece(from, us); - if (!!captured_piece) RemovePiece(to, them); - PlacePiece(to, promoted_to, us); - - for (const auto castling_side : - {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { - const auto their_rook = rook_positions_[static_cast(them)] - [static_cast(castling_side)]; - if (to == their_rook) { - irreversible_data_.castling_rights[static_cast(them)] &= - ~static_cast( - kCastlingRightsForSide[static_cast(castling_side)]); - } - } -} - -void Position::DoMove(const Castling &move) { - const auto [side, king_from, rook_from] = move; - - const auto us = side_to_move_; - - const auto color_idx = static_cast(us); - const auto side_idx = static_cast(side); - - RemovePiece(king_from, us); - RemovePiece(rook_from, us); - PlacePiece(kKingCastlingDestination[color_idx][side_idx], Piece::kKing, us); - PlacePiece(kRookCastlingDestination[color_idx][side_idx], Piece::kRook, us); - - king_position_[static_cast(us)] = - kKingCastlingDestination[color_idx][side_idx]; - irreversible_data_.castling_rights[static_cast(us)] = 0; -} void Position::UndoMove(const Move &move, const IrreversibleData &data) { const auto &ep_square = irreversible_data_.en_croissant_square; @@ -249,19 +225,69 @@ void Position::UndoMove(const Move &move, const IrreversibleData &data) { if (ep_square.has_value()) { hash_ ^= hasher_.en_croissant_hash[GetCoordinates(ep_square.value()).first]; } + + const auto from = move.From(); + const auto to = move.To(); + const auto them = side_to_move_; // Current side (after the move was made) + const auto us = Flip(them); // Side that made the move + + // Get captured piece from current irreversible data before restoring + const auto captured_piece = irreversible_data_.captured_piece; + irreversible_data_ = data; for (const auto color : {Player::kWhite, Player::kBlack}) { hash_ ^= hasher_.cr_hash[static_cast( color)][irreversible_data_.castling_rights[static_cast(color)] .to_ulong()]; } - if (ep_square.has_value()) { - hash_ ^= hasher_.en_croissant_hash[GetCoordinates(ep_square.value()).first]; + if (irreversible_data_.en_croissant_square.has_value()) { + hash_ ^= hasher_.en_croissant_hash[GetCoordinates(irreversible_data_.en_croissant_square.value()).first]; } hash_ ^= hasher_.stm_hash; side_to_move_ = Flip(side_to_move_); - std::visit([this](const auto &unwrapped_move) { UndoMove(unwrapped_move); }, - move); + + if (move.IsEnPassant()) { + const auto capture_square = Shift(to, kPawnMoveDirection[static_cast(them)]); + MovePiece(to, from, us); + PlacePiece(capture_square, Piece::kPawn, them); + } else if (move.IsPromotion()) { + RemovePiece(to, us); + if (!!captured_piece) PlacePiece(to, captured_piece, them); + PlacePiece(from, Piece::kPawn, us); + } else if (move.IsCastling()) { + const auto color_idx = static_cast(us); + + // Determine which side based on king destination + Castling::CastlingSide side; + if (to == kKingCastlingDestination[color_idx][0]) { + side = Castling::CastlingSide::k00; + } else { + side = Castling::CastlingSide::k000; + } + + const auto side_idx = static_cast(side); + const auto rook_from = rook_positions_[color_idx][side_idx]; + + // Remove pieces from castled positions first + RemovePiece(kKingCastlingDestination[color_idx][side_idx], us); + RemovePiece(kRookCastlingDestination[color_idx][side_idx], us); + + // Place pieces back at original positions + PlacePiece(from, Piece::kKing, us); + PlacePiece(rook_from, Piece::kRook, us); + + king_position_[color_idx] = from; + } else { + // Normal move + const auto piece_to_move = board_[to]; + + MovePiece(to, from, us); + if (!!captured_piece) PlacePiece(to, captured_piece, them); + + if (piece_to_move == Piece::kKing) { + king_position_[static_cast(us)] = from; + } + } history_stack_.Pop(); } @@ -279,78 +305,6 @@ void SimpleChessEngine::Position::UndoMove(NullMove, history_stack_.Pop(); } -void Position::UndoMove(const DefaultMove &move) { - const auto [from, to, captured_piece] = move; - - const Player us = side_to_move_; - const Player them = Flip(us); - - const auto piece_to_move = board_[to]; - - MovePiece(to, from, us); - if (!!captured_piece) PlacePiece(to, captured_piece, them); - - if (piece_to_move == Piece::kKing) - king_position_[static_cast(us)] = from; -} - -void Position::UndoMove(const PawnPush &move) { - const auto [from, to] = move; - - const auto us = side_to_move_; - - MovePiece(to, from, us); -} - -void Position::UndoMove(const DoublePush &move) { - const auto from = move.from; - - const auto us = side_to_move_; - - const auto to = move.to; - - MovePiece(to, from, us); -} - -void Position::UndoMove(const EnCroissant &move) { - const auto [from, to] = move; - - const auto us = side_to_move_; - const auto them = Flip(us); - - const auto capture_square = - Shift(to, kPawnMoveDirection[static_cast(them)]); - - MovePiece(to, from, us); - PlacePiece(capture_square, Piece::kPawn, them); -} - -void Position::UndoMove(const Promotion &move) { - const auto [from, to, captured_piece] = static_cast(move); - - const auto us = side_to_move_; - const auto them = Flip(us); - - RemovePiece(to, us); - if (!!captured_piece) PlacePiece(to, captured_piece, them); - PlacePiece(from, Piece::kPawn, us); -} - -void Position::UndoMove(const Castling &move) { - const auto [side, king_from, rook_from] = move; - - const auto us = side_to_move_; - - const auto color_idx = static_cast(us); - const auto side_idx = static_cast(side); - - PlacePiece(king_from, Piece::kKing, us); - PlacePiece(rook_from, Piece::kRook, us); - RemovePiece(kKingCastlingDestination[color_idx][side_idx], us); - RemovePiece(kRookCastlingDestination[color_idx][side_idx], us); - - king_position_[static_cast(us)] = king_from; -} [[nodiscard]] bool Position::CanCastle( const Castling::CastlingSide castling_side) const { @@ -479,17 +433,17 @@ bool Position::StaticExchangeEvaluation(const Move &move, const auto [from, to, captured_piece] = GetMoveData(move); - const auto promotion_or = std::get_if(&move); - - Piece next_victim = - !promotion_or ? GetPieceAt(from) : promotion_or->promoted_to; + Piece next_victim = GetPieceAt(from); + + if (move.IsPromotion()) { + next_victim = move.PromotionPiece(); + } Eval balance = EstimatePiece(captured_piece); - if (promotion_or) { - balance += - EstimatePiece(promotion_or->promoted_to) - EstimatePiece(Piece::kPawn); - } else if (std::holds_alternative(move)) { + if (move.IsPromotion()) { + balance += EstimatePiece(move.PromotionPiece()) - EstimatePiece(Piece::kPawn); + } else if (move.IsEnPassant()) { balance = EstimatePiece(Piece::kPawn); } @@ -505,12 +459,10 @@ bool Position::StaticExchangeEvaluation(const Move &move, return true; } - Bitboard occupancy = - GetAllPieces() ^ SingleSquare(from) ^ SingleSquare(to); + Bitboard occupancy = GetAllPieces() ^ SingleSquare(from) ^ SingleSquare(to); - [[unlikely]] if (std::holds_alternative(move)) { - occupancy ^= Shift(SingleSquare(to), - kPawnMoveDirection[static_cast(them)]); + [[unlikely]] if (move.IsEnPassant()) { + occupancy ^= Shift(SingleSquare(to), kPawnMoveDirection[static_cast(them)]); } Bitboard attackers = Attackers(to, ~occupancy) & occupancy; diff --git a/Chess/Position.h b/Chess/Position.h index e2d22fc..fb14aca 100644 --- a/Chess/Position.h +++ b/Chess/Position.h @@ -60,10 +60,12 @@ class Position { pinners{}; //!< Pieces that are pinning opponent's pieces. std::array blockers{}; //!< Pieces that are blocking attacks on the king. + + Piece captured_piece{Piece::kNone}; //!< Piece captured by the last move bool operator==(const IrreversibleData &other) const { - return std::tie(en_croissant_square, castling_rights) == - std::tie(other.en_croissant_square, other.castling_rights); + return std::tie(en_croissant_square, castling_rights, captured_piece) == + std::tie(other.en_croissant_square, other.castling_rights, other.captured_piece); } }; @@ -269,18 +271,6 @@ class Position { void MovePiece(const BitIndex from, const BitIndex to, const Player color); - void DoMove(const DefaultMove &move); - void DoMove(const PawnPush &move); - void DoMove(const DoublePush &move); - void DoMove(const EnCroissant &move); - void DoMove(const Promotion &move); - void DoMove(const Castling &move); - void UndoMove(const DefaultMove &move); - void UndoMove(const PawnPush &move); - void UndoMove(const DoublePush &move); - void UndoMove(const EnCroissant &move); - void UndoMove(const Promotion &move); - void UndoMove(const Castling &move); void SetCastlingRights(const std::array, 2> &castling_rights); diff --git a/Chess/Quiescence.cpp b/Chess/Quiescence.cpp index b0d1539..c0cc0cb 100644 --- a/Chess/Quiescence.cpp +++ b/Chess/Quiescence.cpp @@ -30,18 +30,26 @@ template SearchResult Quiescence::Search( auto kCompareMoves = [](const Move& lhs, const Move& rhs, const Position& current_position) { - if (lhs.index() != rhs.index()) return lhs.index() > rhs.index(); - if (!std::holds_alternative(lhs)) return false; - const auto [from_lhs, to_lhs, captured_piece_lhs] = GetMoveData(lhs); - const auto [from_rhs, to_rhs, captured_piece_rhs] = GetMoveData(rhs); - const auto captured_idx_lhs = static_cast(captured_piece_lhs); - const auto captured_idx_rhs = static_cast(captured_piece_rhs); - const auto moving_idx_lhs = - -static_cast(current_position.GetPieceAt(from_lhs)); - const auto moving_idx_rhs = - -static_cast(current_position.GetPieceAt(from_rhs)); - return std::tie(captured_idx_lhs, moving_idx_lhs) > - std::tie(captured_idx_rhs, moving_idx_rhs); + // Compare move types: promotions > en passant > castling > normal + const auto lhs_type = static_cast(lhs.Type()); + const auto rhs_type = static_cast(rhs.Type()); + if (lhs_type != rhs_type) return lhs_type > rhs_type; + + // For normal moves, compare by captured piece value (MVV-LVA) + if (lhs.IsNormal()) { + const auto [from_lhs, to_lhs, captured_piece_lhs] = GetMoveData(lhs); + const auto [from_rhs, to_rhs, captured_piece_rhs] = GetMoveData(rhs); + const auto captured_idx_lhs = static_cast(captured_piece_lhs); + const auto captured_idx_rhs = static_cast(captured_piece_rhs); + const auto moving_idx_lhs = + -static_cast(current_position.GetPieceAt(from_lhs)); + const auto moving_idx_rhs = + -static_cast(current_position.GetPieceAt(from_rhs)); + return std::tie(captured_idx_lhs, moving_idx_lhs) > + std::tie(captured_idx_rhs, moving_idx_rhs); + } + + return false; }; template diff --git a/Chess/Searcher.h b/Chess/Searcher.h index 1229c71..b798d4a 100644 --- a/Chess/Searcher.h +++ b/Chess/Searcher.h @@ -36,8 +36,8 @@ class Searcher { template requires StopSearchCondition friend struct SearchNode; - constexpr static size_t kTTsize = 1 << 25; - using SearcherTranspositionTable = TranspositionTable; + constexpr static size_t kTTSizeInMb = 512; + using SearcherTranspositionTable = TranspositionTable; /** * \brief Constructor. diff --git a/Chess/StreamUtility.h b/Chess/StreamUtility.h index 9502db9..af150e5 100644 --- a/Chess/StreamUtility.h +++ b/Chess/StreamUtility.h @@ -49,72 +49,18 @@ inline std::ostream& PrintCoordinates(const Coordinates coordinates, return stream; } -inline std::ostream& operator<<(std::ostream& stream, const DefaultMove& move) { - const auto from = GetCoordinates(move.from); - const auto to = GetCoordinates(move.to); - - PrintCoordinates(from, stream); - PrintCoordinates(to, stream); - - return stream; -} -inline std::ostream& operator<<(std::ostream& stream, const PawnPush& move) { - const auto from = GetCoordinates(move.from); - const auto to = GetCoordinates(move.to); - - PrintCoordinates(from, stream); - PrintCoordinates(to, stream); - - return stream; -} -inline std::ostream& operator<<(std::ostream& stream, const DoublePush& move) { - const auto from = GetCoordinates(move.from); - const auto to = GetCoordinates(move.to); - - PrintCoordinates(from, stream); - PrintCoordinates(to, stream); - - return stream; -} -inline std::ostream& operator<<(std::ostream& stream, const EnCroissant& move) { - const auto from = GetCoordinates(move.from); - const auto to = GetCoordinates(move.to); - - PrintCoordinates(from, stream); - PrintCoordinates(to, stream); - - return stream; -} -inline std::ostream& operator<<(std::ostream& stream, const Promotion& move) { - const auto from = GetCoordinates(move.from); - const auto to = GetCoordinates(move.to); +inline std::ostream& operator<<(std::ostream& stream, const Move& move) { + const auto from = GetCoordinates(move.From()); + const auto to = GetCoordinates(move.To()); PrintCoordinates(from, stream); PrintCoordinates(to, stream); - stream << kPiecesChars[static_cast(move.promoted_to)]; + if (move.IsPromotion()) { + stream << kPiecesChars[static_cast(move.PromotionPiece())]; + } return stream; } -inline std::ostream& operator<<(std::ostream& stream, const Castling& move) { - const auto from = GetCoordinates(move.king_from); - - static constexpr std::array kCastlingShifts = {Compass::kEast, - Compass::kWest}; - - const auto shift = kCastlingShifts[static_cast(move.side)]; - - const auto to = GetCoordinates(Shift(Shift(move.king_from, shift), shift)); - - PrintCoordinates(from, stream); - PrintCoordinates(to, stream); - - return stream; -} - -inline std::ostream& operator<<(std::ostream& stream, const Move& move) { - std::visit([&](const auto& move) { stream << move; }, move); - return stream; -} } // namespace SimpleChessEngine \ No newline at end of file diff --git a/Chess/TranspositionTable.h b/Chess/TranspositionTable.h index 6dd3cb3..8532144 100644 --- a/Chess/TranspositionTable.h +++ b/Chess/TranspositionTable.h @@ -28,10 +28,11 @@ struct Node { }; #pragma pack(pop) -template - requires(std::has_single_bit(TableSize)) +template class TranspositionTable { public: + static constexpr size_t kTableSize = (TableSizeMB * 1024 * 1024) / sizeof(Node); + [[nodiscard]] bool Contains(const Position& position) const { return position.GetHash() == GetNode(position).true_hash; } @@ -52,13 +53,13 @@ class TranspositionTable { } Node& GetNode(const Position& position) { - return table_[position.GetHash() % TableSize]; + return table_[position.GetHash() % kTableSize]; } const Node& GetNode(const Position& position) const { - return table_[position.GetHash() % TableSize]; + return table_[position.GetHash() % kTableSize]; } - std::vector table_ = std::vector(TableSize); //!< The table. + std::vector table_ = std::vector(kTableSize); //!< The table. }; } // namespace SimpleChessEngine diff --git a/Tests/CompactMoveTests.cpp b/Tests/CompactMoveTests.cpp new file mode 100644 index 0000000..6ad2cd9 --- /dev/null +++ b/Tests/CompactMoveTests.cpp @@ -0,0 +1,145 @@ +#include + +#include "Move.h" + +using namespace SimpleChessEngine; + +namespace CompactMoveTests { + +TEST(BasicConstruction, FromToSquares) { + const Move move(0, 8); + ASSERT_EQ(move.From(), 0); + ASSERT_EQ(move.To(), 8); + ASSERT_TRUE(move.IsNormal()); + ASSERT_TRUE(move.IsValid()); +} + +TEST(PromotionMoves, AllPieces) { + const Move queen_promo = Move::Make(48, 56, Piece::kQueen); + ASSERT_TRUE(queen_promo.IsPromotion()); + ASSERT_EQ(queen_promo.PromotionPiece(), Piece::kQueen); + + const Move knight_promo = Move::Make(48, 56, Piece::kKnight); + ASSERT_EQ(knight_promo.PromotionPiece(), Piece::kKnight); + + const Move rook_promo = Move::Make(48, 56, Piece::kRook); + ASSERT_EQ(rook_promo.PromotionPiece(), Piece::kRook); + + const Move bishop_promo = Move::Make(48, 56, Piece::kBishop); + ASSERT_EQ(bishop_promo.PromotionPiece(), Piece::kBishop); +} + +TEST(SpecialMoves, EnPassantAndCastling) { + const Move en_passant = Move::Make(32, 41); + ASSERT_TRUE(en_passant.IsEnPassant()); + ASSERT_FALSE(en_passant.IsPromotion()); + ASSERT_FALSE(en_passant.IsCastling()); + ASSERT_FALSE(en_passant.IsNormal()); + + const Move castling = Move::Make(4, 6); + ASSERT_TRUE(castling.IsCastling()); + ASSERT_FALSE(castling.IsEnPassant()); + ASSERT_FALSE(castling.IsPromotion()); + ASSERT_FALSE(castling.IsNormal()); +} + +TEST(MoveEquality, SameAndDifferent) { + const Move move1(8, 16); + const Move move2(8, 16); + const Move move3(8, 24); + + ASSERT_EQ(move1, move2); + ASSERT_NE(move1, move3); +} + +TEST(SpecialValues, NullAndNone) { + ASSERT_FALSE(Move::Null().IsValid()); + ASSERT_FALSE(Move::None().IsValid()); + ASSERT_NE(Move::Null(), Move::None()); +} + +TEST(BitLayout, CorrectEncoding) { + const Move move(63, 0); + ASSERT_EQ(move.Raw() & 0x3F, 0); + ASSERT_EQ((move.Raw() >> 6) & 0x3F, 63); + ASSERT_EQ((move.Raw() >> 14) & 0x3, 0); +} + +TEST(PromotionBitLayout, CorrectTypeAndPiece) { + const Move queen_promo = Move::Make(48, 56, Piece::kQueen); + ASSERT_EQ((queen_promo.Raw() >> 14) & 0x3, 1); + ASSERT_EQ((queen_promo.Raw() >> 12) & 0x3, + static_cast(Piece::kQueen) - static_cast(Piece::kKnight)); +} + +TEST(HashFunction, Consistency) { + const Move move1(8, 16); + const Move move2(8, 16); + const Move move3(8, 24); + + Move::Hash hasher; + ASSERT_EQ(hasher(move1), hasher(move2)); + ASSERT_NE(hasher(move1), hasher(move3)); +} + +TEST(SizeAndAlignment, OptimalLayout) { + ASSERT_EQ(sizeof(Move), 2); + ASSERT_EQ(alignof(Move), 2); + ASSERT_TRUE(std::is_trivially_copyable_v); + ASSERT_TRUE(std::is_standard_layout_v); +} + +TEST(BooleanConversion, ValidityCheck) { + const Move valid_move(8, 16); + const Move none_move = Move::None(); + + ASSERT_TRUE(static_cast(valid_move)); + ASSERT_FALSE(static_cast(none_move)); +} + +TEST(RawDataAccess, RoundTrip) { + const Move move(8, 16); + const std::uint16_t raw = move.Raw(); + const Move reconstructed(raw); + + ASSERT_EQ(move, reconstructed); + ASSERT_EQ(move.From(), reconstructed.From()); + ASSERT_EQ(move.To(), reconstructed.To()); +} + +TEST(AllSquares, ValidEncoding) { + for (BitIndex from = 0; from < 64; ++from) { + for (BitIndex to = 0; to < 64; ++to) { + if (from != to) { + const Move move(from, to); + ASSERT_EQ(move.From(), from); + ASSERT_EQ(move.To(), to); + ASSERT_TRUE(move.IsValid()); + } + } + } +} + +TEST(LegacyCompatibility, GetMoveData) { + const Move normal_move(8, 16); + const auto [from, to, captured] = GetMoveData(normal_move); + ASSERT_EQ(from, 8); + ASSERT_EQ(to, 16); + ASSERT_EQ(captured, Piece::kNone); + + const Move en_passant = Move::Make(32, 41); + const auto [ep_from, ep_to, ep_captured] = GetMoveData(en_passant); + ASSERT_EQ(ep_from, 32); + ASSERT_EQ(ep_to, 41); + ASSERT_EQ(ep_captured, Piece::kPawn); +} + +TEST(LegacyCompatibility, IsQuietFunction) { + const Move normal_move(8, 16); + ASSERT_TRUE(IsQuiet(normal_move)); + + const Move en_passant = Move::Make(32, 41); + ASSERT_FALSE(IsQuiet(en_passant)); +} + +} // namespace CompactMoveTests \ No newline at end of file diff --git a/Tests/MoveGeneratorTests.cpp b/Tests/MoveGeneratorTests.cpp index bda8b45..b6678be 100644 --- a/Tests/MoveGeneratorTests.cpp +++ b/Tests/MoveGeneratorTests.cpp @@ -37,10 +37,10 @@ struct GameInfo { if (depth == 1) { for (const auto &move : moves) { - if (std::get_if(&move)) { + if (move.IsEnPassant()) { answer.en_croissants.value()++; } - if (std::get_if(&move)) { + if (move.IsCastling()) { answer.castlings.value()++; } } From a0e652fe834b2ef20d6f8d853429904b94dc9836 Mon Sep 17 00:00:00 2001 From: nook0110 Date: Sat, 3 Jan 2026 00:58:58 +0300 Subject: [PATCH 2/5] fix --- Chess/Searcher.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chess/Searcher.h b/Chess/Searcher.h index b798d4a..34cc9d0 100644 --- a/Chess/Searcher.h +++ b/Chess/Searcher.h @@ -36,7 +36,7 @@ class Searcher { template requires StopSearchCondition friend struct SearchNode; - constexpr static size_t kTTSizeInMb = 512; + constexpr static size_t kTTSizeInMb = 640; using SearcherTranspositionTable = TranspositionTable; /** From c10d96a332671b7cbf49381653f9e2a3894ad4e3 Mon Sep 17 00:00:00 2001 From: nook0110 Date: Sat, 3 Jan 2026 19:52:34 +0300 Subject: [PATCH 3/5] fixes --- Chess/Move.h | 28 +++---------------------- Chess/MoveGenerator.cpp | 2 +- Chess/MovePicker.cpp | 12 ++++++++++- Chess/Position.cpp | 40 +++++++++++++++++++++++------------- Chess/Position.h | 6 +++--- Chess/PositionFactory.h | 8 ++++---- Chess/Quiescence.cpp | 24 ---------------------- Chess/SearchImplementation.h | 22 +++++++++++++++----- Chess/TranspositionTable.h | 2 +- Tests/ChessEngineTests.cpp | 5 +++++ Tests/CompactMoveTests.cpp | 30 --------------------------- 11 files changed, 71 insertions(+), 108 deletions(-) diff --git a/Chess/Move.h b/Chess/Move.h index ba5818c..3dd9396 100644 --- a/Chess/Move.h +++ b/Chess/Move.h @@ -11,9 +11,9 @@ struct NullMove {}; enum class MoveType : std::uint16_t { kNormal = 0, - kPromotion = 1 << 14, + kCastling = 1 << 14, kEnPassant = 2 << 14, - kCastling = 3 << 14 + kPromotion = 3 << 14, }; class Move { @@ -84,29 +84,7 @@ class Move { std::uint16_t data_; }; -// Legacy compatibility functions -inline std::tuple GetMoveData(const Move& move) { - if (move.IsEnPassant()) { - return {move.From(), move.To(), Piece::kPawn}; - } - if (move.IsCastling()) { - return {move.From(), 64, Piece::kNone}; - } - return {move.From(), move.To(), Piece::kNone}; -} - -inline bool IsQuiet(const Move& move) { - return move.IsNormal() && !move.IsEnPassant(); -} - -inline bool DoesReset(const Move& move) { - return !move.IsCastling(); -} - -// Castling side enum for position management -struct Castling { - enum class CastlingSide : std::uint8_t { k00, k000 }; -}; +enum class CastlingSide : std::uint8_t { k00, k000 }; static_assert(sizeof(Move) == 2, "Move must be exactly 2 bytes"); static_assert(alignof(Move) == 2, "Move should be 2-byte aligned"); diff --git a/Chess/MoveGenerator.cpp b/Chess/MoveGenerator.cpp index 5afdb45..23fc5e5 100644 --- a/Chess/MoveGenerator.cpp +++ b/Chess/MoveGenerator.cpp @@ -61,7 +61,7 @@ void MoveGenerator::GenerateCastling(Moves& moves, const Position& position) { const auto color_idx = static_cast(side_to_move); for (const auto castling_side : - {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { + {CastlingSide::k00, CastlingSide::k000}) { if (position.CanCastle(castling_side)) { const auto side_idx = static_cast(castling_side); const auto king_to = kKingCastlingDestination[color_idx][side_idx]; diff --git a/Chess/MovePicker.cpp b/Chess/MovePicker.cpp index 951ae58..cb3865c 100644 --- a/Chess/MovePicker.cpp +++ b/Chess/MovePicker.cpp @@ -138,7 +138,17 @@ void MovePicker::InitPicker(MoveGenerator::Moves&& moves, history_.resize(moves_.size()); for (size_t i = 0; i < moves_.size(); ++i) { const auto& move = moves_[i]; - const auto [from, to, capture] = GetMoveData(move); + const auto from = move.From(); + const auto to = move.To(); + + // Determine captured piece + Piece capture = Piece::kNone; + if (move.IsEnPassant()) { + capture = Piece::kPawn; + } else if (!move.IsCastling()) { + capture = searcher.GetPosition().GetPieceAt(to); + } + data_[i] = {from, to, capture, capture != Piece::kNone ? searcher.GetPosition().StaticExchangeEvaluation( diff --git a/Chess/Position.cpp b/Chess/Position.cpp index fe4330b..7bf7bf7 100644 --- a/Chess/Position.cpp +++ b/Chess/Position.cpp @@ -1,7 +1,6 @@ #include "Position.h" #include -#include #include "Attacks.h" #include "BitBoard.h" @@ -123,7 +122,7 @@ void Position::DoMove(const Move &move) { if (!!captured_piece) RemovePiece(to, them); PlacePiece(to, promoted_to, us); - for (const auto castling_side : {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { + for (const auto castling_side : {CastlingSide::k00, CastlingSide::k000}) { const auto their_rook = rook_positions_[static_cast(them)][static_cast(castling_side)]; if (to == their_rook) { irreversible_data_.castling_rights[static_cast(them)] &= @@ -138,14 +137,14 @@ void Position::DoMove(const Move &move) { // Determine which side based on king destination const auto color_idx = static_cast(us); - Castling::CastlingSide side; + CastlingSide side; BitIndex rook_from; if (king_to == kKingCastlingDestination[color_idx][0]) { - side = Castling::CastlingSide::k00; + side = CastlingSide::k00; rook_from = rook_positions_[color_idx][0]; } else { - side = Castling::CastlingSide::k000; + side = CastlingSide::k000; rook_from = rook_positions_[color_idx][1]; } @@ -177,7 +176,7 @@ void Position::DoMove(const Move &move) { irreversible_data_.castling_rights[static_cast(us)] = 0; } - for (auto castling_side : {Castling::CastlingSide::k00, Castling::CastlingSide::k000}) { + for (auto castling_side : {CastlingSide::k00, CastlingSide::k000}) { const auto our_rook = rook_positions_[static_cast(us)][static_cast(castling_side)]; const auto their_rook = rook_positions_[static_cast(them)][static_cast(castling_side)]; if (from == our_rook) { @@ -199,7 +198,11 @@ void Position::DoMove(const Move &move) { side_to_move_ = Flip(side_to_move_); hash_ ^= hasher_.stm_hash; - history_stack_.Push(hash_, DoesReset(move)); + // 50-move rule: reset counter if it's a pawn move or a capture + const bool resets_fifty_move = + (board_[to] == Piece::kPawn) || // Moving piece is a pawn + (irreversible_data_.captured_piece != Piece::kNone); // Any capture + history_stack_.Push(hash_, resets_fifty_move); } void SimpleChessEngine::Position::DoMove(NullMove) { @@ -258,11 +261,11 @@ void Position::UndoMove(const Move &move, const IrreversibleData &data) { const auto color_idx = static_cast(us); // Determine which side based on king destination - Castling::CastlingSide side; + CastlingSide side; if (to == kKingCastlingDestination[color_idx][0]) { - side = Castling::CastlingSide::k00; + side = CastlingSide::k00; } else { - side = Castling::CastlingSide::k000; + side = CastlingSide::k000; } const auto side_idx = static_cast(side); @@ -307,7 +310,7 @@ void SimpleChessEngine::Position::UndoMove(NullMove, [[nodiscard]] bool Position::CanCastle( - const Castling::CastlingSide castling_side) const { + const CastlingSide castling_side) const { const auto us = side_to_move_; const auto us_idx = static_cast(us); const auto cs_idx = static_cast(castling_side); @@ -371,7 +374,7 @@ Bitboard Position::GetPiecesByType(const Player player) const { } template -Bitboard Position::GetCastlingSquares(Castling::CastlingSide side) const { +Bitboard Position::GetCastlingSquares(CastlingSide side) const { static_assert(piece == Piece::kRook || piece == Piece::kKing); if constexpr (piece == Piece::kKing) { return castling_squares_for_king_[static_cast(side_to_move_)] @@ -402,7 +405,7 @@ BitIndex Position::GetKingSquare(const Player player) const { } BitIndex Position::GetCastlingRookSquare(Player player, - Castling::CastlingSide side) const { + CastlingSide side) const { return rook_positions_[static_cast(player)] [static_cast(side)]; } @@ -431,7 +434,16 @@ bool Position::StaticExchangeEvaluation(const Move &move, const auto us = side_to_move_; const auto them = Flip(us); - const auto [from, to, captured_piece] = GetMoveData(move); + const auto from = move.From(); + const auto to = move.To(); + + // Get captured piece from the board + Piece captured_piece = Piece::kNone; + if (move.IsEnPassant()) { + captured_piece = Piece::kPawn; + } else if (!move.IsCastling()) { + captured_piece = GetPieceAt(to); + } Piece next_victim = GetPieceAt(from); diff --git a/Chess/Position.h b/Chess/Position.h index fb14aca..c6ca8b4 100644 --- a/Chess/Position.h +++ b/Chess/Position.h @@ -125,7 +125,7 @@ class Position { void UndoMove(NullMove, const IrreversibleData &data); [[nodiscard]] bool CanCastle( - const Castling::CastlingSide castling_side) const; + const CastlingSide castling_side) const; /** * \brief Gets hash of the position. @@ -181,7 +181,7 @@ class Position { [[nodiscard]] BitIndex GetKingSquare(Player player) const; [[nodiscard]] BitIndex GetCastlingRookSquare( - Player player, Castling::CastlingSide side) const; + Player player, CastlingSide side) const; [[nodiscard]] Bitboard Attackers(BitIndex square, Bitboard transparent = kEmptyBoard) const; @@ -221,7 +221,7 @@ class Position { const; template - [[nodiscard]] Bitboard GetCastlingSquares(Castling::CastlingSide side) const; + [[nodiscard]] Bitboard GetCastlingSquares(CastlingSide side) const; [[nodiscard]] IrreversibleData GetIrreversibleData() const; diff --git a/Chess/PositionFactory.h b/Chess/PositionFactory.h index 888071f..9691756 100644 --- a/Chess/PositionFactory.h +++ b/Chess/PositionFactory.h @@ -156,19 +156,19 @@ inline std::string SimpleChessEngine::FenFactory::operator()( const auto castling_rights = position.GetCastlingRights(); if (castling_rights[static_cast(Player::kWhite)].test( - static_cast(Castling::CastlingSide::k00))) { + static_cast(CastlingSide::k00))) { ss << 'K'; } if (castling_rights[static_cast(Player::kWhite)].test( - static_cast(Castling::CastlingSide::k000))) { + static_cast(CastlingSide::k000))) { ss << 'Q'; } if (castling_rights[static_cast(Player::kBlack)].test( - static_cast(Castling::CastlingSide::k00))) { + static_cast(CastlingSide::k00))) { ss << 'k'; } if (castling_rights[static_cast(Player::kBlack)].test( - static_cast(Castling::CastlingSide::k000))) { + static_cast(CastlingSide::k000))) { ss << 'q'; } diff --git a/Chess/Quiescence.cpp b/Chess/Quiescence.cpp index c0cc0cb..fc81687 100644 --- a/Chess/Quiescence.cpp +++ b/Chess/Quiescence.cpp @@ -28,30 +28,6 @@ template SearchResult Quiescence::Search( Position& current_position, Eval alpha, const Eval beta, const Depth current_depth); -auto kCompareMoves = [](const Move& lhs, const Move& rhs, - const Position& current_position) { - // Compare move types: promotions > en passant > castling > normal - const auto lhs_type = static_cast(lhs.Type()); - const auto rhs_type = static_cast(rhs.Type()); - if (lhs_type != rhs_type) return lhs_type > rhs_type; - - // For normal moves, compare by captured piece value (MVV-LVA) - if (lhs.IsNormal()) { - const auto [from_lhs, to_lhs, captured_piece_lhs] = GetMoveData(lhs); - const auto [from_rhs, to_rhs, captured_piece_rhs] = GetMoveData(rhs); - const auto captured_idx_lhs = static_cast(captured_piece_lhs); - const auto captured_idx_rhs = static_cast(captured_piece_rhs); - const auto moving_idx_lhs = - -static_cast(current_position.GetPieceAt(from_lhs)); - const auto moving_idx_rhs = - -static_cast(current_position.GetPieceAt(from_rhs)); - return std::tie(captured_idx_lhs, moving_idx_lhs) > - std::tie(captured_idx_rhs, moving_idx_rhs); - } - - return false; -}; - template requires StopSearchCondition template diff --git a/Chess/SearchImplementation.h b/Chess/SearchImplementation.h index a18f913..eb28c93 100644 --- a/Chess/SearchImplementation.h +++ b/Chess/SearchImplementation.h @@ -189,7 +189,11 @@ SearchResult SearchNode::operator()() { if (static_cast(entry_bound) & Bound::kLower && entry_score > alpha) { if (entry_score >= beta) { - if (IsQuiet(hash_move)) { + // Check if it's a quiet move (no capture, not en passant, not promotion) + const auto to = hash_move.To(); + const bool is_quiet = hash_move.IsNormal() && + searcher_.GetPosition().GetPieceAt(to) == Piece::kNone; + if (is_quiet) { UpdateQuietMove(hash_move); } @@ -379,8 +383,14 @@ std::optional SearchNode::CheckFirstMove( if (iteration_status_.best_eval > alpha) { if (iteration_status_.best_eval >= beta) { assert(iteration_status_.best_move); - if (IsQuiet(*iteration_status_.best_move)) { - UpdateQuietMove(*iteration_status_.best_move); + // Check if it's a quiet move (no capture, not en passant, not promotion) + // Note: We check the position BEFORE the move was made (stored in position_info_) + const auto& best_move = *iteration_status_.best_move; + const auto to = best_move.To(); + const bool is_quiet = best_move.IsNormal() && + searcher_.GetPosition().GetPieceAt(to) == Piece::kNone; + if (is_quiet) { + UpdateQuietMove(best_move); } return true; @@ -542,14 +552,16 @@ void SearchNode::UpdateQuietMove(const Move &move) { if constexpr (!is_first_move) { for (auto it = move_picker_.begin_quiet(); it != move_picker_.current(); ++it) { - const auto [from, to, captured_piece] = GetMoveData(*it); + const auto from = it->From(); + const auto to = it->To(); searcher_.history_[position_info_.side_to_move_idx][from][to] -= state_.remaining_depth * state_.remaining_depth; } } // history bonus - const auto [from, to, captured_piece] = GetMoveData(move); + const auto from = move.From(); + const auto to = move.To(); searcher_.history_[position_info_.side_to_move_idx][from][to] += state_.remaining_depth * state_.remaining_depth; diff --git a/Chess/TranspositionTable.h b/Chess/TranspositionTable.h index 8532144..1b5739f 100644 --- a/Chess/TranspositionTable.h +++ b/Chess/TranspositionTable.h @@ -31,7 +31,7 @@ struct Node { template class TranspositionTable { public: - static constexpr size_t kTableSize = (TableSizeMB * 1024 * 1024) / sizeof(Node); + static constexpr size_t kTableSize = 1 << 25; [[nodiscard]] bool Contains(const Position& position) const { return position.GetHash() == GetNode(position).true_hash; diff --git a/Tests/ChessEngineTests.cpp b/Tests/ChessEngineTests.cpp index 64e8bcf..e402e84 100644 --- a/Tests/ChessEngineTests.cpp +++ b/Tests/ChessEngineTests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "Move.h" #include "MoveFactory.h" @@ -20,6 +21,10 @@ struct BestMoveTestCase { std::string best_move; }; +std::ostream& operator<<(std::ostream& os, const BestMoveTestCase& test_case) { + return os << "FEN: " << test_case.fen << ", Expected move: " << test_case.best_move; +} + class BestMoveTest : public testing::TestWithParam { protected: [[nodiscard]] Move GetMove() const { diff --git a/Tests/CompactMoveTests.cpp b/Tests/CompactMoveTests.cpp index 6ad2cd9..0fef689 100644 --- a/Tests/CompactMoveTests.cpp +++ b/Tests/CompactMoveTests.cpp @@ -65,13 +65,6 @@ TEST(BitLayout, CorrectEncoding) { ASSERT_EQ((move.Raw() >> 14) & 0x3, 0); } -TEST(PromotionBitLayout, CorrectTypeAndPiece) { - const Move queen_promo = Move::Make(48, 56, Piece::kQueen); - ASSERT_EQ((queen_promo.Raw() >> 14) & 0x3, 1); - ASSERT_EQ((queen_promo.Raw() >> 12) & 0x3, - static_cast(Piece::kQueen) - static_cast(Piece::kKnight)); -} - TEST(HashFunction, Consistency) { const Move move1(8, 16); const Move move2(8, 16); @@ -119,27 +112,4 @@ TEST(AllSquares, ValidEncoding) { } } } - -TEST(LegacyCompatibility, GetMoveData) { - const Move normal_move(8, 16); - const auto [from, to, captured] = GetMoveData(normal_move); - ASSERT_EQ(from, 8); - ASSERT_EQ(to, 16); - ASSERT_EQ(captured, Piece::kNone); - - const Move en_passant = Move::Make(32, 41); - const auto [ep_from, ep_to, ep_captured] = GetMoveData(en_passant); - ASSERT_EQ(ep_from, 32); - ASSERT_EQ(ep_to, 41); - ASSERT_EQ(ep_captured, Piece::kPawn); -} - -TEST(LegacyCompatibility, IsQuietFunction) { - const Move normal_move(8, 16); - ASSERT_TRUE(IsQuiet(normal_move)); - - const Move en_passant = Move::Make(32, 41); - ASSERT_FALSE(IsQuiet(en_passant)); -} - } // namespace CompactMoveTests \ No newline at end of file From b7b2eca3fe3d52012db72307dd86e3414c572328 Mon Sep 17 00:00:00 2001 From: nook0110 Date: Sat, 3 Jan 2026 20:02:52 +0300 Subject: [PATCH 4/5] fix --- Chess/Move.h | 7 +++++++ Chess/SearchImplementation.h | 16 +++------------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Chess/Move.h b/Chess/Move.h index 3dd9396..61a1990 100644 --- a/Chess/Move.h +++ b/Chess/Move.h @@ -65,6 +65,13 @@ class Move { constexpr bool IsEnPassant() const { return Type() == MoveType::kEnPassant; } constexpr bool IsCastling() const { return Type() == MoveType::kCastling; } constexpr bool IsNormal() const { return Type() == MoveType::kNormal; } + + template + bool IsQuiet(const Position& position) const { + if (IsPromotion() || IsEnPassant()) return false; + if (IsCastling()) return true; + return position.GetPieceAt(To()) == Piece::kNone; + } static constexpr Move Null() { return Move(kNullValue); } static constexpr Move None() { return Move(kNoneValue); } diff --git a/Chess/SearchImplementation.h b/Chess/SearchImplementation.h index eb28c93..a634ef2 100644 --- a/Chess/SearchImplementation.h +++ b/Chess/SearchImplementation.h @@ -189,11 +189,7 @@ SearchResult SearchNode::operator()() { if (static_cast(entry_bound) & Bound::kLower && entry_score > alpha) { if (entry_score >= beta) { - // Check if it's a quiet move (no capture, not en passant, not promotion) - const auto to = hash_move.To(); - const bool is_quiet = hash_move.IsNormal() && - searcher_.GetPosition().GetPieceAt(to) == Piece::kNone; - if (is_quiet) { + if (hash_move.IsQuiet(searcher_.GetPosition())) { UpdateQuietMove(hash_move); } @@ -383,14 +379,8 @@ std::optional SearchNode::CheckFirstMove( if (iteration_status_.best_eval > alpha) { if (iteration_status_.best_eval >= beta) { assert(iteration_status_.best_move); - // Check if it's a quiet move (no capture, not en passant, not promotion) - // Note: We check the position BEFORE the move was made (stored in position_info_) - const auto& best_move = *iteration_status_.best_move; - const auto to = best_move.To(); - const bool is_quiet = best_move.IsNormal() && - searcher_.GetPosition().GetPieceAt(to) == Piece::kNone; - if (is_quiet) { - UpdateQuietMove(best_move); + if (iteration_status_.best_move->IsQuiet(searcher_.GetPosition())) { + UpdateQuietMove(*iteration_status_.best_move); } return true; From b567c7923a7113a6b320362e23f309f2209f794b Mon Sep 17 00:00:00 2001 From: nook0110 Date: Sat, 3 Jan 2026 21:29:48 +0300 Subject: [PATCH 5/5] fix --- Chess/Position.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Chess/Position.cpp b/Chess/Position.cpp index 7bf7bf7..ec0144f 100644 --- a/Chess/Position.cpp +++ b/Chess/Position.cpp @@ -234,7 +234,6 @@ void Position::UndoMove(const Move &move, const IrreversibleData &data) { const auto them = side_to_move_; // Current side (after the move was made) const auto us = Flip(them); // Side that made the move - // Get captured piece from current irreversible data before restoring const auto captured_piece = irreversible_data_.captured_piece; irreversible_data_ = data; @@ -260,7 +259,6 @@ void Position::UndoMove(const Move &move, const IrreversibleData &data) { } else if (move.IsCastling()) { const auto color_idx = static_cast(us); - // Determine which side based on king destination CastlingSide side; if (to == kKingCastlingDestination[color_idx][0]) { side = CastlingSide::k00; @@ -271,11 +269,9 @@ void Position::UndoMove(const Move &move, const IrreversibleData &data) { const auto side_idx = static_cast(side); const auto rook_from = rook_positions_[color_idx][side_idx]; - // Remove pieces from castled positions first RemovePiece(kKingCastlingDestination[color_idx][side_idx], us); RemovePiece(kRookCastlingDestination[color_idx][side_idx], us); - // Place pieces back at original positions PlacePiece(from, Piece::kKing, us); PlacePiece(rook_from, Piece::kRook, us); @@ -437,7 +433,6 @@ bool Position::StaticExchangeEvaluation(const Move &move, const auto from = move.From(); const auto to = move.To(); - // Get captured piece from the board Piece captured_piece = Piece::kNone; if (move.IsEnPassant()) { captured_piece = Piece::kPawn;