diff --git a/CMakePresets.json b/CMakePresets.json index df02ac82..1fd84fc5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -128,11 +128,11 @@ "cacheVariables": { "CMAKE_CXX_FLAGS_DEBUG": { "type": "STRING", - "value": "-fsanitize=address -fsanitize-address-use-after-scope -fno-omit-frame-pointer" + "value": "-g -fsanitize=address -fsanitize-address-use-after-scope -fno-omit-frame-pointer" }, "CMAKE_C_FLAGS_DEBUG": { "type": "STRING", - "value": "-fsanitize=address -fsanitize-address-use-after-scope -fno-omit-frame-pointer" + "value": "-g -fsanitize=address -fsanitize-address-use-after-scope -fno-omit-frame-pointer" } }, "displayName": "Build with Ninja & Clang (ASAN)", @@ -143,11 +143,11 @@ "cacheVariables": { "CMAKE_CXX_FLAGS_DEBUG": { "type": "STRING", - "value": "-fsanitize=undefined" + "value": "-g -fsanitize=undefined" }, "CMAKE_C_FLAGS_DEBUG": { "type": "STRING", - "value": "-fsanitize=undefined" + "value": "-g -fsanitize=undefined" } }, "displayName": "Build with Ninja & Clang (UBSAN)", diff --git a/libbenbot/src/search/Callbacks.cpp b/libbenbot/src/search/Callbacks.cpp index 7dd4bf4b..7d380da3 100644 --- a/libbenbot/src/search/Callbacks.cpp +++ b/libbenbot/src/search/Callbacks.cpp @@ -15,6 +15,7 @@ #include #include #include // IWYU pragma: keep - for std::abs() +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -57,7 +59,7 @@ namespace { using std::string; - enum class Alignment { + enum class Alignment : std::uint_least8_t { Left, Right, Center @@ -105,14 +107,14 @@ namespace { std::cout << get_column_text(text); } - template + template [[nodiscard]] auto get_duration_string( const milliseconds duration) -> std::optional { static constexpr auto msPerUnit = duration_cast(Duration { 1uz }); if (duration >= msPerUnit) { - using FractionalDuration = std::chrono::duration; + using FractionalDuration = chess::util::FractionalDuration; return std::format( "{:.2%Q %q}", @@ -195,7 +197,7 @@ namespace { void print_score( const Score& score) { - enum class ScoreType { + enum class ScoreType : std::uint_least8_t { Winning, Losing, Equal diff --git a/libchess/include/libchess/notation/ICCF.hpp b/libchess/include/libchess/notation/ICCF.hpp new file mode 100644 index 00000000..78dd01f4 --- /dev/null +++ b/libchess/include/libchess/notation/ICCF.hpp @@ -0,0 +1,57 @@ +/* + * ====================================================================================== + * + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░▒▓████████▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░ + * + * ====================================================================================== + */ + +/** @file + This file provides functions for converting Move objects to and from + ICCF-format numeric notation. + + @ingroup notation + */ + +#pragma once + +#include +#include +#include +#include + +namespace chess::game { +struct Position; +} // namespace chess::game + +namespace chess::notation { + +using game::Position; +using moves::Move; + +/** Returns the ICCF-format algebraic notation for the given Move object. + + @ingroup notation + @see from_iccf() + */ +[[nodiscard]] auto to_iccf(Move move) -> std::string; + +/** Parses the ICCF-format algebraic notation string into a Move object. + The current position is used to determine the type of the moved piece. + + If the input string cannot be parsed correctly, returns an explanatory error string. + + @ingroup notation + @see to_iccf() + */ +[[nodiscard]] auto from_iccf( + const Position& position, std::string_view text) + -> std::expected; + +} // namespace chess::notation diff --git a/libchess/include/libchess/util/Chrono.hpp b/libchess/include/libchess/util/Chrono.hpp new file mode 100644 index 00000000..f2f6dba1 --- /dev/null +++ b/libchess/include/libchess/util/Chrono.hpp @@ -0,0 +1,55 @@ +/* + * ====================================================================================== + * + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░▒▓████████▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░ + * + * ====================================================================================== + */ + +/** @file + This file provides some utilities for working with the ``std::chrono`` library. + @ingroup util + */ + +#pragma once + +#include +#include + +namespace chess::util { + +namespace detail { + template + inline constexpr bool IsChronoDuration = false; + + template + inline constexpr bool IsChronoDuration> = true; +} // namespace detail + +/** This concept matches any specialization of ``std::chrono::duration``. + + @ingroup util + */ +template +concept ChronoDuration = detail::IsChronoDuration; + +/** This typedef allows converting a chrono duration to one with the same period, + but with a floating-point tick type. + + Example usage: + @code{.cpp} + using PartialSeconds = FractionalDuration; + + PartialSeconds secs { 1.5f }; + @endcode + */ +template +using FractionalDuration = std::chrono::duration; + +} // namespace chess::util diff --git a/libchess/src/notation/ICCF.cpp b/libchess/src/notation/ICCF.cpp new file mode 100644 index 00000000..7c6fc0ac --- /dev/null +++ b/libchess/src/notation/ICCF.cpp @@ -0,0 +1,195 @@ +/* + * ====================================================================================== + * + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░▒▓████████▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░ + * + * ====================================================================================== + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chess::notation { + +namespace { + using board::Square; + using std::string; + using PieceType = pieces::Type; + + [[nodiscard, gnu::const]] auto digit_to_char(std::integral auto digit) -> char + { + assert(digit >= 0 and digit <= 9); + + return static_cast( + static_cast('0') + static_cast(digit)); + } + + [[nodiscard]] auto to_iccf_string(const Square square) + -> string + { + return { + digit_to_char(std::to_underlying(square.file) + 1), + digit_to_char(std::to_underlying(square.rank) + 1) + }; + } + + [[nodiscard, gnu::const]] auto to_iccf_char(const PieceType piece) -> char + { + switch (piece) { + case PieceType::Queen : return '1'; + case PieceType::Rook : return '2'; + case PieceType::Bishop: return '3'; + case PieceType::Knight: return '4'; + case PieceType::Pawn : [[fallthrough]]; + case PieceType::King : [[fallthrough]]; + default : std::unreachable(); + } + } +} // namespace + +auto to_iccf(Move move) -> string +{ + return move.promoted_type() + .transform([move](const PieceType promotedType) { + return std::format( + "{}{}{}", + to_iccf_string(move.from()), + to_iccf_string(move.to()), + to_iccf_char(promotedType)); + }) + .or_else([move] { + return std::make_optional( + std::format( + "{}{}", + to_iccf_string(move.from()), + to_iccf_string(move.to()))); + }) + .value(); +} + +namespace { + using board::BitboardIndex; + + [[nodiscard, gnu::const]] auto digit_from_char(const char input) + -> std::expected + { + switch (input) { + case '0': return BitboardIndex { 0 }; + case '1': return BitboardIndex { 1 }; + case '2': return BitboardIndex { 2 }; + case '3': return BitboardIndex { 3 }; + case '4': return BitboardIndex { 4 }; + case '5': return BitboardIndex { 5 }; + case '6': return BitboardIndex { 6 }; + case '7': return BitboardIndex { 7 }; + case '8': return BitboardIndex { 8 }; + case '9': return BitboardIndex { 9 }; + default : return std::unexpected { + std::format("Cannot parse digit from input: {}", input) + }; + } + } + + [[nodiscard, gnu::const]] auto parse_square(const std::string_view input) + -> std::expected + { + if (input.size() != 2uz) + return std::unexpected { + std::format("Cannot parse Square from string: {}", input) + }; + + return digit_from_char(input.front()) + .and_then([input](const BitboardIndex fileNum) { + return digit_from_char(input.back()) + .and_then([fileNum](const BitboardIndex rankNum) { + return std::expected { + Square { + .file = static_cast(fileNum - 1), + .rank = static_cast(rankNum - 1) } + }; + }); + }); + } + + [[nodiscard, gnu::const]] auto parse_piece_type(const char input) + -> std::expected + { + switch (input) { + case '1': return PieceType::Queen; + case '2': return PieceType::Rook; + case '3': return PieceType::Bishop; + case '4': return PieceType::Knight; + default : return std::unexpected { + std::format("Cannot parse promoted type from input: {}", input) + }; + } + } +} // namespace + +using MoveOrError = std::expected; + +auto from_iccf( + const Position& position, std::string_view text) + -> MoveOrError +{ + text = util::strings::trim(text); + + if (text.length() < 4uz) + return std::unexpected { + "Expected at least 4 characters for ICCF notation" + }; + + return parse_square(text.substr(0uz, 2uz)) + .and_then([&text, &position](const Square fromSq) { + text = text.substr(2uz); + + return parse_square(text.substr(0uz, 2uz)) + .and_then([&text, &position, fromSq](const Square toSq) -> MoveOrError { + text = text.substr(2uz); + + return position.our_pieces() + .get_piece_on(fromSq) + .transform([text, fromSq, toSq](const PieceType movedType) -> MoveOrError { + if (text.empty()) { + // non-promotion + return Move { fromSq, toSq, movedType }; + } + + // promotion + assert(movedType == PieceType::Pawn); + + return parse_piece_type(text.front()) + .transform([fromSq, toSq](const PieceType promotedType) { + return Move { fromSq, toSq, PieceType::Pawn, promotedType }; + }); + }) + .value_or( + std::unexpected { + std::format( + "No piece for color {} can move from square {}", + magic_enum::enum_name(position.sideToMove), fromSq) }); + }); + }); +} + +} // namespace chess::notation diff --git a/libchess/src/notation/UCI.cpp b/libchess/src/notation/UCI.cpp index ef29efe2..5b7a5db4 100644 --- a/libchess/src/notation/UCI.cpp +++ b/libchess/src/notation/UCI.cpp @@ -12,6 +12,7 @@ * ====================================================================================== */ +#include #include #include #include @@ -81,13 +82,17 @@ auto from_uci( return position.our_pieces() .get_piece_on(from) .transform([text, from, dest](const PieceType movedType) -> MoveOrError { - if (text.empty()) + if (text.empty()) { + // non-promotion return Move { from, dest, movedType }; + } // promotion + assert(movedType == PieceType::Pawn); + return pieces::from_string(text) - .transform([from, dest, movedType](const PieceType promotedType) { - return Move { from, dest, movedType, promotedType }; + .transform([from, dest](const PieceType promotedType) { + return Move { from, dest, PieceType::Pawn, promotedType }; }) .transform_error([](const string_view parseError) { return std::format( diff --git a/libchess/src/uci/Printing.cpp b/libchess/src/uci/Printing.cpp index 799642bb..b41fcfd7 100644 --- a/libchess/src/uci/Printing.cpp +++ b/libchess/src/uci/Printing.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -84,7 +85,7 @@ auto SearchInfo::Score::MateIn::moves() const noexcept -> int auto SearchInfo::get_nps() const noexcept -> size_t { - using FractionalSeconds = std::chrono::duration; + using FractionalSeconds = util::FractionalDuration; const auto seconds = duration_cast(time).count(); diff --git a/tests/unit/libchess/notation/ICCF.cpp b/tests/unit/libchess/notation/ICCF.cpp new file mode 100644 index 00000000..83e8325a --- /dev/null +++ b/tests/unit/libchess/notation/ICCF.cpp @@ -0,0 +1,132 @@ +/* + * ====================================================================================== + * + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░▒▓████████▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░ + * + * ====================================================================================== + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +inline constexpr auto TAGS { "[notation][ICCF]" }; + +using PieceType = chess::pieces::Type; +using chess::board::File; +using chess::board::Rank; +using chess::board::Square; +using chess::game::Position; +using chess::notation::from_fen; +using chess::notation::from_iccf; +using chess::notation::to_iccf; + +TEST_CASE("ICCF notation", TAGS) +{ + const Position startingPosition { }; + + SECTION("Pawn push: e4") + { + const auto move = from_iccf(startingPosition, "5254").value(); + + REQUIRE(move.piece() == PieceType::Pawn); + REQUIRE(move.to() == Square { File::E, Rank::Four }); + REQUIRE(move.from() == Square { File::E, Rank::Two }); + + REQUIRE(to_iccf(move) == "5254"); + } + + SECTION("Promotion to rook") + { + const auto pos = from_fen("8/5P2/2k5/8/8/8/1K6/8 w - - 0 1").value(); + + const auto move = from_iccf(pos, "67682").value(); + + REQUIRE(move.piece() == PieceType::Pawn); + REQUIRE(move.to() == Square { File::F, Rank::Eight }); + REQUIRE(move.from() == Square { File::F, Rank::Seven }); + + REQUIRE(move.is_promotion()); + REQUIRE(move.promoted_type().value() == PieceType::Rook); + + REQUIRE(to_iccf(move) == "67682"); + } +} + +TEST_CASE("ICCF notation: castling", TAGS) +{ + const auto pos = from_fen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1").value(); + + SECTION("Kingside") + { + SECTION("White") + { + const auto move = from_iccf(pos, "5171").value(); + + REQUIRE(move.piece() == PieceType::King); + REQUIRE(move.to() == Square { File::G, Rank::One }); + REQUIRE(move.from() == Square { File::E, Rank::One }); + + REQUIRE(move.is_castling()); + + REQUIRE(to_iccf(move) == "5171"); + } + + SECTION("Black") + { + const auto move = from_iccf( + after_null_move(pos), "5878") + .value(); + + REQUIRE(move.piece() == PieceType::King); + REQUIRE(move.to() == Square { File::G, Rank::Eight }); + REQUIRE(move.from() == Square { File::E, Rank::Eight }); + + REQUIRE(move.is_castling()); + + REQUIRE(to_iccf(move) == "5878"); + } + } + + SECTION("Queenside") + { + SECTION("White") + { + const auto move = from_iccf(pos, "5131").value(); + + REQUIRE(move.piece() == PieceType::King); + REQUIRE(move.to() == Square { File::C, Rank::One }); + REQUIRE(move.from() == Square { File::E, Rank::One }); + + REQUIRE(move.is_castling()); + + REQUIRE(to_iccf(move) == "5131"); + } + + SECTION("Black") + { + const auto move = from_iccf( + after_null_move(pos), "5838") + .value(); + + REQUIRE(move.piece() == PieceType::King); + REQUIRE(move.to() == Square { File::C, Rank::Eight }); + REQUIRE(move.from() == Square { File::E, Rank::Eight }); + + REQUIRE(move.is_castling()); + + REQUIRE(to_iccf(move) == "5838"); + } + } +}