diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 807cfb94..77b3a84d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -92,7 +92,7 @@ repos: # C++ formatting & linting - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.8 + rev: v22.1.0 hooks: - id: clang-format exclude_types: [json] diff --git a/CMakeLists.txt b/CMakeLists.txt index 935a5cf6..b15a1ca6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,25 @@ cpack_add_component_group ( ben_bot_all DISPLAY_NAME "BenBot" DESCRIPTION "All components of BenBot" EXPANDED BOLD_TITLE ) +FetchContent_Declare ( + termcolor + SYSTEM + GIT_REPOSITORY "https://github.com/ikalnytskyi/termcolor.git" + GIT_TAG v2.1.0 + GIT_SHALLOW ON + SOURCE_SUBDIR foo # we need to disable adding termcolor as a subdirectory, because they list a + # too-old cmake version + FIND_PACKAGE_ARGS 2.1.0 +) + +FetchContent_MakeAvailable (termcolor) + +if (termcolor_SOURCE_DIR) + set (TERMCOLOR_INCLUDES "${termcolor_SOURCE_DIR}/include") +elseif (termcolor_DIR) + set (TERMCOLOR_INCLUDES "${termcolor_DIR}/include") +endif () + add_subdirectory (libchess) add_subdirectory (libbenbot) add_subdirectory (ben-bot) diff --git a/ben-bot/CMakeLists.txt b/ben-bot/CMakeLists.txt index 06091514..d88b17ce 100644 --- a/ben-bot/CMakeLists.txt +++ b/ben-bot/CMakeLists.txt @@ -12,27 +12,32 @@ add_subdirectory (resources) -set (benbot_sources - # cmake-format: sortable - src/Bench.cpp src/CLI.cpp src/Engine.cpp src/main.cpp src/Perft.cpp src/Printing.cpp -) - -add_executable (ben_bot ${benbot_sources}) +add_executable (ben_bot) target_compile_features (ben_bot PRIVATE cxx_std_23) target_link_libraries (ben_bot PRIVATE ben_bot::libbenbot ben_bot::resources) +set_property ( + SOURCE src/ColorPrinting.cpp APPEND PROPERTY INCLUDE_DIRECTORIES "${TERMCOLOR_INCLUDES}" +) + set_target_properties ( ben_bot PROPERTIES CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF DEBUG_POSTFIX -d ) -set (benbot_headers - # cmake-format: sortable - include/ben-bot/CLI.hpp include/ben-bot/CustomCommand.hpp include/ben-bot/Engine.hpp +file (GLOB_RECURSE benbot_headers LIST_DIRECTORIES false CONFIGURE_DEPENDS + "${CMAKE_CURRENT_LIST_DIR}/include/*.hpp" ) -target_sources (ben_bot PRIVATE FILE_SET HEADERS BASE_DIRS include FILES ${benbot_headers}) +file (GLOB benbot_sources LIST_DIRECTORIES false CONFIGURE_DEPENDS + "${CMAKE_CURRENT_LIST_DIR}/src/*.cpp" +) + +target_sources ( + ben_bot PRIVATE FILE_SET HEADERS BASE_DIRS include FILES ${benbot_headers} + PRIVATE ${benbot_sources} +) source_group (TREE "${CMAKE_CURRENT_LIST_DIR}" FILES ${benbot_headers} ${benbot_sources}) @@ -50,9 +55,9 @@ set ( "Apple developer ID string, for example 'Developer ID Application: Your Name (2DO8NL92GO)'" ) -add_feature_info ( - benbot_codesigning "CODESIGN_PROGRAM AND CODESIGN_ID" "Signing of BenBot binaries" -) +include (FeatureSummary) + +add_feature_info (codesigning "CODESIGN_PROGRAM AND CODESIGN_ID" "Signing of BenBot binaries") if (CODESIGN_PROGRAM AND CODESIGN_ID) add_custom_command ( diff --git a/ben-bot/include/ben-bot/ColorPrinting.hpp b/ben-bot/include/ben-bot/ColorPrinting.hpp new file mode 100644 index 00000000..8227b007 --- /dev/null +++ b/ben-bot/include/ben-bot/ColorPrinting.hpp @@ -0,0 +1,55 @@ +/* + * ====================================================================================== + * + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░▒▓████████▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░ + * + * ====================================================================================== + */ + +/** @file + This file provides functions for colored printing. + + @ingroup benbot + */ + +#pragma once + +#include + +namespace chess::util::strings { +struct TextTable; +} // namespace chess::util::strings + +namespace chess::game { +struct Position; +} // namespace chess::game + +namespace ben_bot { + +using chess::game::Position; +using std::string_view; + +/** @ingroup benbot + @{ + */ + +/** Prints the given table with bold headings, faint outlines, + and regular content cells. + */ +void print_colored_table(const chess::util::strings::TextTable& table); + +/** Prints the given position with faint file/rank labels. */ +void print_colored_board(const Position& pos, bool utf8); + +/** Prints the given label faintly, and the info as regular text. */ +void print_labeled_info(string_view label, string_view info); + +/** @} */ + +} // namespace ben_bot diff --git a/ben-bot/include/ben-bot/Engine.hpp b/ben-bot/include/ben-bot/Engine.hpp index cf181656..ef752b2a 100644 --- a/ben-bot/include/ben-bot/Engine.hpp +++ b/ben-bot/include/ben-bot/Engine.hpp @@ -81,14 +81,21 @@ class [[nodiscard]] Engine final : public uci::EngineBase { void print_options(string_view args) const; void print_current_position(string_view arguments) const; + void set_pretty_printing(bool shouldPrettyPrint); + static void print_compiler_info(); static void start_file_logger(string_view path); + static constexpr bool PRETTY_PRINT_DEFAULT = false; + std::atomic_bool debugMode { false }; + std::atomic_bool prettyPrinting { PRETTY_PRINT_DEFAULT }; search::Thread searcher; + /* ----- UCI options ----- */ + uci::IntOption ttSize { "Hash", 1, 2048, 16, @@ -129,10 +136,21 @@ class [[nodiscard]] Engine final : public uci::EngineBase { uci::StringOption logFile { "Debug Log File", "", "If not empty, engine I/O will be mirrored to this file", - [](const string_view path) { start_file_logger(path); } + start_file_logger + }; + + uci::BoolOption prettyPrintMode { + "Pretty Print", + PRETTY_PRINT_DEFAULT, + "When on, search output is pretty-printed instead of printed in UCI format", + [this](const bool usePretty) { set_pretty_printing(usePretty); } + }; + + std::array options { + &ttSize, &clearTT, &ponder, &threads, &moveOverhead, &logFile, &prettyPrintMode }; - std::array options { &ttSize, &clearTT, &ponder, &threads, &moveOverhead, &logFile }; + /* ----- Custom commands ----- */ // clang-format off std::array customCommands { diff --git a/ben-bot/resources/src/BuildTime.cpp b/ben-bot/resources/src/BuildTime.cpp index dae075c3..30864b53 100644 --- a/ben-bot/resources/src/BuildTime.cpp +++ b/ben-bot/resources/src/BuildTime.cpp @@ -132,7 +132,7 @@ namespace { [[nodiscard, gnu::cold]] auto to_utc_time(const std::time_t time) -> std::tm { - std::tm ret {}; + std::tm ret { }; // this is written this way to avoid using the thread-unsafe function gmtime() // gmtime_s() doesn't appear to be present on MacOS, and Windows's argument order diff --git a/ben-bot/src/Bench.cpp b/ben-bot/src/Bench.cpp index d3d02f02..a3cf44b1 100644 --- a/ben-bot/src/Bench.cpp +++ b/ben-bot/src/Bench.cpp @@ -96,6 +96,7 @@ namespace { // clang-format off search::Thread thread { search::Callbacks { + .onSearchStart = nullptr, .onSearchComplete = [this](const SearchResult& res) { print_info(res); result = res; }, .onIteration = [this](const SearchResult& res) { print_info(res); } } @@ -161,13 +162,13 @@ namespace { const auto totalNodes = std::transform_reduce( results.begin(), results.end(), 0uz, - std::plus {}, + std::plus { }, [](const SearchResult& result) { return result.nodesSearched; }); const auto totalTime = std::transform_reduce( results.begin(), results.end(), milliseconds { 0 }, - std::plus {}, + std::plus { }, [](const SearchResult& result) { return result.duration; }); const auto seconds = static_cast(totalTime.count()) * 0.001; diff --git a/ben-bot/src/ColorPrinting.cpp b/ben-bot/src/ColorPrinting.cpp new file mode 100644 index 00000000..1de58959 --- /dev/null +++ b/ben-bot/src/ColorPrinting.cpp @@ -0,0 +1,109 @@ +/* + * ====================================================================================== + * + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░▒▓████████▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░ + * + * ====================================================================================== + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ben_bot { + +using std::cout; +using std::string_view; + +namespace { + template + [[nodiscard, gnu::const]] auto get_at_most_n_lines(const string_view input) + { + using Lines = beman::inplace_vector::inplace_vector; + + return chess::util::strings::lines_view(input) + | std::views::take(MaxLines) + | std::ranges::to(); + } +} // namespace + +void Engine::print_logo_and_version() const +{ + const auto logoLines = get_at_most_n_lines<11uz>(resources::get_ascii_logo()); + + cout << termcolor::grey << logoLines.front() << '\n' + << termcolor::blue; + + for (const auto line : logoLines | std::views::drop(1) | std::views::take(logoLines.capacity() - 2uz)) + cout << line << '\n'; + + cout << termcolor::grey << logoLines.back() << "\n\n" + << termcolor::reset << termcolor::bold << get_name() << ", " + << termcolor::reset << "by " << get_author() << '\n' + << termcolor::reset; +} + +void print_colored_table(const chess::util::strings::TextTable& table) +{ + table.print( + [](const string_view heading) { + // we want the heading text to be underlined, but not the + // whitespace that follows the text to complete the cell + const auto trimmed = chess::util::strings::trim(heading); + + cout << termcolor::bold << termcolor::underline + << trimmed + << termcolor::reset; + + const auto numSpaces = heading.length() - trimmed.length(); + + for (auto i = 0uz; i < numSpaces; ++i) + cout << ' '; + }, + [](const string_view cell) { + cout << cell; + }, + [](const string_view outline) { + cout << termcolor::white << outline << termcolor::reset; + }, + [] { cout << '\n'; }); + + cout << termcolor::reset; +} + +void print_colored_board(const Position& pos, const bool utf8) +{ + const auto boardStr = utf8 ? print_utf8(pos) : print_ascii(pos); + + const auto lines = get_at_most_n_lines<9uz>(boardStr); + + for (const auto line : lines | std::views::take(lines.capacity() - 1uz)) { + cout << line.substr(0uz, line.length() - 1uz) + << termcolor::white << line.back() << '\n' + << termcolor::reset; + } + + cout << termcolor::white << lines.back() << '\n' + << termcolor::reset; +} + +void print_labeled_info(const string_view label, const string_view info) +{ + cout << termcolor::white << label << termcolor::reset << info << '\n'; +} + +} // namespace ben_bot diff --git a/ben-bot/src/Engine.cpp b/ben-bot/src/Engine.cpp index 9051f5db..cf222b40 100644 --- a/ben-bot/src/Engine.cpp +++ b/ben-bot/src/Engine.cpp @@ -26,6 +26,7 @@ namespace ben_bot { +using std::memory_order_relaxed; using std::size_t; using uci::printing::info_string; @@ -39,8 +40,30 @@ void Engine::new_game(const bool firstCall) // we use delayed initialization for these callbacks instead of // initializing them in the constructor to avoid referencing the // `this` pointer in the constructor - searcher.context.callbacks = search::Callbacks::make_uci_printer( - [this] { return debugMode.load(std::memory_order::relaxed); }); + if (prettyPrinting.load()) { + searcher.context.callbacks = search::Callbacks::make_pretty_printer(); + } else { + searcher.context.callbacks = search::Callbacks::make_uci_printer( + [this] noexcept { // cppcheck-suppress syntaxError + return debugMode.load(memory_order_relaxed); + }); + } +} + +void Engine::set_pretty_printing(const bool shouldPrettyPrint) +{ + // check if the requested printing mode was already active + if (prettyPrinting.exchange(shouldPrettyPrint, memory_order_relaxed) == shouldPrettyPrint) + return; + + wait(); + + if (shouldPrettyPrint) { + searcher.context.callbacks = search::Callbacks::make_pretty_printer(); + } else { + searcher.context.callbacks = search::Callbacks::make_uci_printer( + [this] noexcept { return debugMode.load(memory_order_relaxed); }); + } } void Engine::go(const uci::GoCommandOptions& opts) diff --git a/ben-bot/src/Printing.cpp b/ben-bot/src/Printing.cpp index 1e474e3a..37ba8c37 100644 --- a/ben-bot/src/Printing.cpp +++ b/ben-bot/src/Printing.cpp @@ -12,6 +12,7 @@ * ====================================================================================== */ +#include #include #include #include @@ -45,15 +46,6 @@ auto Engine::get_name() const -> std::string return std::format("BenBot {}", resources::get_version_string()); } -void Engine::print_logo_and_version() const -{ - println("{}", resources::get_ascii_logo()); - - println( - "{}, by {}", - get_name(), get_author()); -} - void Engine::print_help(const string_view args) const { const bool noLogo = [args] { @@ -85,7 +77,7 @@ void Engine::print_help(const string_view args) const .append_column(command.description); } - println("{}", table.to_string()); + print_colored_table(table); } void Engine::print_options(const string_view args) const @@ -134,7 +126,9 @@ void Engine::print_options(const string_view args) const } } - println("{}", table.to_string()); + print_colored_table(table); + + println(""); if (not noCurrent) println("Debug mode: {}", debugMode.load()); @@ -144,28 +138,31 @@ void Engine::print_current_position(const string_view arguments) const { const auto& pos = searcher.context.options.position; - const bool utf8 = trim(arguments) == "utf8"; - - println("{}", - utf8 ? print_utf8(pos) : print_ascii(pos)); + print_colored_board(pos, + trim(arguments) == "utf8"); println(""); - info_string(std::format("FEN: {}", chess::notation::to_fen(pos))); - info_string(std::format("Zobrist key: {}", pos.hash)); - searcher.context.transTable.find(pos) + print_labeled_info("FEN: ", chess::notation::to_fen(pos)); + + print_labeled_info("Zobrist key: ", std::format("{}", pos.hash)); + + print_labeled_info("Static eval: ", std::format("{}", eval::evaluate(pos))); + + searcher.context.transTable + .find(pos) .transform([](const TTData& data) { - info_string(std::format( - "TT hit: depth {} eval {} type {} probed {} bestmove {}", - data.searchedDepth, data.eval, - magic_enum::enum_name(data.evalType), - eval::Score::from_tt(data.eval, 0uz), - chess::notation::to_uci(data.bestMove.value_or(Move {})))); - - return std::monostate {}; + print_labeled_info( + "TT hit: ", + std::format( + "depth {} eval {} type {} probed {} bestmove {}", + data.searchedDepth, data.eval, + magic_enum::enum_name(data.evalType), + eval::Score::from_tt(data.eval, 0uz), + chess::notation::to_uci(data.bestMove.value_or(Move { })))); + + return std::monostate { }; }); - - info_string(std::format("Static eval: {}", eval::evaluate(pos))); } void Engine::print_compiler_info() diff --git a/libbenbot/CMakeLists.txt b/libbenbot/CMakeLists.txt index f9bcf921..d6f562fe 100644 --- a/libbenbot/CMakeLists.txt +++ b/libbenbot/CMakeLists.txt @@ -16,6 +16,10 @@ target_compile_features (libbenbot PUBLIC cxx_std_23) target_link_libraries (libbenbot PUBLIC ben_bot::libchess) +set_property ( + SOURCE src/search/Callbacks.cpp APPEND PROPERTY INCLUDE_DIRECTORIES "${TERMCOLOR_INCLUDES}" +) + set_target_properties ( libbenbot PROPERTIES CXX_STANDARD_REQUIRED ON diff --git a/libbenbot/include/libbenbot/data-structures/KillerMoves.hpp b/libbenbot/include/libbenbot/data-structures/KillerMoves.hpp index fa8e5fb8..67f19d28 100644 --- a/libbenbot/include/libbenbot/data-structures/KillerMoves.hpp +++ b/libbenbot/include/libbenbot/data-structures/KillerMoves.hpp @@ -68,7 +68,7 @@ struct KillerMoves final { private: using Killers = std::vector; - std::array lists {}; + std::array lists { }; }; } // namespace ben_bot diff --git a/libbenbot/include/libbenbot/search/Callbacks.hpp b/libbenbot/include/libbenbot/search/Callbacks.hpp index 2bbd746f..f52f4208 100644 --- a/libbenbot/include/libbenbot/search/Callbacks.hpp +++ b/libbenbot/include/libbenbot/search/Callbacks.hpp @@ -23,6 +23,7 @@ namespace ben_bot::search { +struct Options; struct Result; /** This struct encapsulates a set of functions that will be called to @@ -36,6 +37,9 @@ struct Callbacks final { /** Function type that accepts a single Result argument. */ using Callback = std::function; + /** Function object that will be invoked when a new search is started. */ + std::function onSearchStart; + /** Function object that will be invoked with results from a completed search. */ Callback onSearchComplete; @@ -44,6 +48,13 @@ struct Callbacks final { */ Callback onIteration; + /** Can be safely called without checking if ``onSearchStart`` is null. */ + void search_start(const Options& options) const + { + if (onSearchStart != nullptr) + onSearchStart(options); + } + /** Can be safely called without checking if ``onSearchComplete`` is null. */ void search_complete(const Result& result) const { @@ -56,8 +67,10 @@ struct Callbacks final { /** Can be safely called without checking if ``onIteration`` is null. */ void iteration_complete(const Result& result) const { - if (onIteration != nullptr) + if (onIteration != nullptr) { + [[likely]]; onIteration(result); + } } /** Creates a set of callbacks that print UCI-formatted information and bestmove @@ -67,7 +80,15 @@ struct Callbacks final { should be included in the information output. */ [[nodiscard]] static auto make_uci_printer( - std::function isDebugMode) + std::function&& isDebugMode) + -> Callbacks; + + /** Creates a set of callbacks that print search information in a human-readable + table-aligned format. + + @note The output produced by these callbacks does not conform to the UCI protocol! + */ + [[nodiscard]] static auto make_pretty_printer() -> Callbacks; }; diff --git a/libbenbot/src/data-structures/TranspositionTable.cpp b/libbenbot/src/data-structures/TranspositionTable.cpp index 21b37315..6619ff38 100644 --- a/libbenbot/src/data-structures/TranspositionTable.cpp +++ b/libbenbot/src/data-structures/TranspositionTable.cpp @@ -96,7 +96,7 @@ struct TranspositionTable::Entry final { generation = gen; eval = static_cast(data.eval); evalType = data.evalType; - move = data.bestMove.value_or(Move {}); + move = data.bestMove.value_or(Move { }); } }; @@ -105,7 +105,7 @@ inline constexpr auto CACHE_LINE_SIZE = std::hardware_constructive_interference_ struct alignas(CACHE_LINE_SIZE) TranspositionTable::Cluster final { static constexpr auto NumRecords = CACHE_LINE_SIZE / sizeof(Entry); - std::array records {}; + std::array records { }; }; TranspositionTable::TranspositionTable(TranspositionTable&& other) noexcept @@ -162,7 +162,7 @@ void TranspositionTable::resize(const size_t sizeMB) if (table == nullptr) { clusterCount = 0uz; - throw std::bad_alloc {}; + throw std::bad_alloc { }; } std::uninitialized_default_construct_n(table, clusterCount); @@ -171,7 +171,7 @@ void TranspositionTable::resize(const size_t sizeMB) void TranspositionTable::clear() { for (auto i = 0uz; i < clusterCount; ++i) - std::ranges::fill(index_table(i).records, Entry {}); + std::ranges::fill(index_table(i).records, Entry { }); } void TranspositionTable::deallocate() @@ -193,7 +193,7 @@ auto TranspositionTable::hashfull() const -> size_t const auto count = std::transform_reduce( indices.begin(), indices.end(), 0uz, - std::plus {}, + std::plus { }, [this](const size_t idx) { return static_cast(std::ranges::count_if( index_table(idx).records, diff --git a/libbenbot/src/eval/PieceSquareTables.cpp b/libbenbot/src/eval/PieceSquareTables.cpp index c58ae3c1..97466ea3 100644 --- a/libbenbot/src/eval/PieceSquareTables.cpp +++ b/libbenbot/src/eval/PieceSquareTables.cpp @@ -43,7 +43,7 @@ namespace { return std::transform_reduce( indices.begin(), indices.end(), 0, - std::plus {}, + std::plus { }, [table](const auto idx) { assert(idx < table.size()); return table[idx]; diff --git a/libbenbot/src/search/Callbacks.cpp b/libbenbot/src/search/Callbacks.cpp index 0eff73fc..7dd4bf4b 100644 --- a/libbenbot/src/search/Callbacks.cpp +++ b/libbenbot/src/search/Callbacks.cpp @@ -12,22 +12,313 @@ * ====================================================================================== */ +#include +#include +#include // IWYU pragma: keep - for std::abs() +#include #include +#include #include #include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace ben_bot::search { auto Callbacks::make_uci_printer( - std::function isDebugMode) + std::function&& isDebugMode) -> Callbacks { + auto printInfo = [isDebug = std::move(isDebugMode)](const Result& res) { + search_info(res.to_libchess(isDebug())); + }; + return { - .onSearchComplete = [isDebugMode](const Result& res) { - search_info(res.to_libchess(isDebugMode())); + .onSearchStart = nullptr, + .onSearchComplete = [printInfo](const Result& res) { + printInfo(res); chess::uci::printing::best_move(res.best_move(), res.ponder_move()); }, - .onIteration = [isDebugMode](const Result& res) { search_info(res.to_libchess(isDebugMode())); } + .onIteration = printInfo + }; +} + +namespace { + // TODO: do column padding via std::format width specifiers? + + using std::string; + + enum class Alignment { + Left, + Right, + Center + }; + + constexpr auto COLUMN_WIDTH = 10uz; + + template + [[nodiscard]] auto get_column_text( + const std::string_view text) -> string + { + assert(text.size() < COLUMN_WIDTH); + + const auto trimmedInput = text.substr(0uz, COLUMN_WIDTH); + + string padded; + + if constexpr (Align == Alignment::Left) { + padded = trimmedInput; + + padded.resize(COLUMN_WIDTH, ' '); + } else { + auto padding = COLUMN_WIDTH - trimmedInput.size(); + + if constexpr (Align == Alignment::Center) { + padding /= 2uz; + } + + padded.resize(padding, ' '); + + padded.append(trimmedInput); + + if constexpr (Align == Alignment::Center) { + padded.append(COLUMN_WIDTH - padded.size(), ' '); + } + } + + return padded; + } + + template + void print_column_text( + const std::string_view text) + { + std::cout << get_column_text(text); + } + + 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; + + return std::format( + "{:.2%Q %q}", + duration_cast(duration)); + } + + return std::nullopt; + } + + [[nodiscard]] auto format_duration( + const milliseconds duration) -> string + { + // NB. it should be quite rare that a search will run for 1 day or more... + return get_duration_string(duration) + .or_else([duration] { return get_duration_string(duration); }) + .or_else([duration] { return get_duration_string(duration); }) + .or_else([duration] { return std::make_optional(std::format("{:%Q %q}", duration)); }) + .value(); + } + + template + [[nodiscard]] auto get_nodes_string( + const size_t nodes) -> std::optional + { + if (nodes >= Ratio::num) { + const auto display = static_cast(nodes) / static_cast(Ratio::num); + + return std::format( + "{:.{}f}{}", + display, Precision, Suffix); + } + + return std::nullopt; + } + + template + [[nodiscard]] auto format_nodes( + const size_t nodes) -> string + { + return get_nodes_string(nodes) + .or_else([nodes] { return get_nodes_string(nodes); }) + .or_else([nodes] { return std::make_optional(std::format("{}", nodes)); }) + .value(); + } + + [[nodiscard]] auto format_nps(const size_t nps) -> string + { + return std::format( + "{}/s", + format_nodes<1uz>(nps)); // use this function for its transformation of the value to a k/M representation + } + + [[nodiscard]] auto format_hashfull(const size_t permille) -> string + { + return std::format( + "{}%", + permille / 10uz); + } + + using Score = chess::uci::printing::SearchInfo::Score; + + [[nodiscard]] auto format_score( + const Score& score) -> string + { + return std::visit( + chess::util::Visitor { + [](const Score::Centipawns& centipawns) { + return std::format( + "{:+}", + centipawns.value); + }, + [](const Score::MateIn& mate) { + return std::format( + "#{}", + std::abs(mate.moves())); + } }, + score.value); + } + + void print_score( + const Score& score) + { + enum class ScoreType { + Winning, + Losing, + Equal + }; + + const auto type = std::visit( + chess::util::Visitor { + [](const Score::Centipawns& value) { + if (value.value == 0) + return ScoreType::Equal; + + if (value.value > 0) + return ScoreType::Winning; + + return ScoreType::Losing; + }, + [](const Score::MateIn& mate) { + if (mate.plies > 0) + return ScoreType::Winning; + + return ScoreType::Losing; + } }, + score.value); + + switch (type) { + case ScoreType::Winning: + std::cout << termcolor::green; + break; + case ScoreType::Losing: + std::cout << termcolor::red; + break; + case ScoreType::Equal: + std::cout << termcolor::grey; + break; + } + + print_column_text( + format_score(score)); + + std::cout << termcolor::reset; + } + + [[nodiscard]] auto format_pv( + const std::span pv) -> string + { + if (pv.empty()) { + // this is possible if we're checkmated + return { }; + } + + string result; + + for (const auto move : pv) { + result.append(chess::notation::to_uci(move)); + result.append(1uz, ' '); + } + + result.pop_back(); // trim last space + + return result; + } + + void print_table_header() + { + std::cout << termcolor::bold; + + print_column_text("Depth"); + + print_column_text("Time"); + + print_column_text("Nodes"); + + print_column_text("NPS"); + + print_column_text("Hashfull"); + + print_column_text("Score"); + + std::cout << "PV\n" + << termcolor::reset; + } + + void pretty_print(const Result& res) + { + // depth + print_column_text( + std::format("{}/{}", res.depth, res.qDepth)); + + // time + print_column_text( + format_duration(res.duration)); + + // nodes + print_column_text( + format_nodes(res.nodesSearched)); + + const auto libchess = res.to_libchess(false); + + // nodes per second + print_column_text( + format_nps(libchess.get_nps())); + + // hashfull + print_column_text( + format_hashfull(res.hashfull)); + + // score + print_score(libchess.score); + + // PV + std::cout << format_pv(res.pv) << '\n'; + } +} // namespace + +auto Callbacks::make_pretty_printer() + -> Callbacks +{ + return { + .onSearchStart = []([[maybe_unused]] const Options& options) { + print_table_header(); + }, + .onSearchComplete = pretty_print, + .onIteration = pretty_print }; } diff --git a/libbenbot/src/search/Result.cpp b/libbenbot/src/search/Result.cpp index 8777470e..97c6e9d3 100644 --- a/libbenbot/src/search/Result.cpp +++ b/libbenbot/src/search/Result.cpp @@ -25,7 +25,7 @@ namespace { const Result& res, const bool includeDebugInfo) -> std::string { if (not includeDebugInfo) - return {}; + return { }; if (res.nodesSearched == 0uz) { return std::format( diff --git a/libbenbot/src/search/Search.cpp b/libbenbot/src/search/Search.cpp index 770b442b..a52b234f 100644 --- a/libbenbot/src/search/Search.cpp +++ b/libbenbot/src/search/Search.cpp @@ -86,7 +86,7 @@ namespace { } private: - std::array moves {}; + std::array moves { }; size_t length { 0uz }; }; @@ -114,7 +114,7 @@ namespace { [[nodiscard]] auto alpha_beta() -> Score // NOLINT(readability-function-cognitive-complexity) { if (interrupter.should_abort(plyFromRoot)) - return {}; + return { }; transTable.prefetch(position); @@ -122,7 +122,7 @@ namespace { // because the table only contains static evaluations and doesn't consider game // history, so its stored evaluations can't detect threefold repetition draws if (position.is_threefold_repetition()) - return {}; + return { }; if (const auto cutoff = bounds.mate_distance_pruning(plyFromRoot)) { ++stats.mdpCutoffs; @@ -143,7 +143,7 @@ namespace { .evalType = EvalType::Exact, .bestMove = std::nullopt }); - return {}; + return { }; } const bool inCheck = position.is_check(); @@ -208,7 +208,7 @@ namespace { ++stats.nodesSearched; if (interrupter.was_aborted()) - return {}; + return { }; if (eval >= bounds.beta) { transTable.store( @@ -251,7 +251,7 @@ namespace { [[nodiscard]] auto quiescence() -> Score { if (interrupter.should_abort(plyFromRoot) or position.is_draw()) - return {}; + return { }; stats.qDepth = std::max(stats.qDepth, plyFromRoot); @@ -287,7 +287,7 @@ namespace { ++stats.nodesSearched; if (interrupter.was_aborted()) - return {}; + return { }; if (evaluation >= bounds.beta) { ++stats.betaCutoffs; @@ -366,7 +366,7 @@ namespace { killerMoves.clear(); detail::order_moves_for_search( - options.position, options.movesToSearch, transTable, {}); + options.position, options.movesToSearch, transTable, { }); Stats stats; Bounds bounds; @@ -461,6 +461,7 @@ void Context::search() // NOLINT(readability-function-cognitive-complexity) Interrupter interrupter { exitFlag, pondering, options.searchTime, options.infinite }; transTable.new_search(); + callbacks.search_start(options); // if the movesToSearch was empty, then we search all legal moves if (options.movesToSearch.empty()) { diff --git a/libchess/include/libchess/board/Bitboard.hpp b/libchess/include/libchess/board/Bitboard.hpp index 76f1e5ac..921e60ec 100644 --- a/libchess/include/libchess/board/Bitboard.hpp +++ b/libchess/include/libchess/board/Bitboard.hpp @@ -453,7 +453,7 @@ constexpr auto Bitboard::indices() const noexcept { return std::ranges::subrange { detail::BitboardIterator { *this }, - detail::BitboardIterator {}, + detail::BitboardIterator { }, count() }; } diff --git a/libchess/include/libchess/board/Pieces.hpp b/libchess/include/libchess/board/Pieces.hpp index d568b742..b1b8df9d 100644 --- a/libchess/include/libchess/board/Pieces.hpp +++ b/libchess/include/libchess/board/Pieces.hpp @@ -273,7 +273,7 @@ constexpr void Pieces::our_move(const moves::Move move, const Color ourColor) no .transform([move, this](const PieceType type) { get_type(move.piece()).unset(move.from()); get_type(type).set(move.to()); - return std::monostate {}; + return std::monostate { }; }) .or_else([move, movementMask, ourColor, this] { get_type(move.piece()) ^= movementMask; @@ -287,7 +287,7 @@ constexpr void Pieces::our_move(const moves::Move move, const Color ourColor) no occupied ^= castleMask; } - return std::make_optional(std::monostate {}); + return std::make_optional(std::monostate { }); }); } diff --git a/libchess/include/libchess/game/Position.hpp b/libchess/include/libchess/game/Position.hpp index 71067bf6..8451f577 100644 --- a/libchess/include/libchess/game/Position.hpp +++ b/libchess/include/libchess/game/Position.hpp @@ -426,10 +426,10 @@ struct [[nodiscard]] Position final { inline auto Position::empty() -> Position { - Position pos {}; + Position pos { }; - pos.whitePieces = {}; - pos.blackPieces = {}; + pos.whitePieces = { }; + pos.blackPieces = { }; pos.refresh_zobrist(); diff --git a/libchess/include/libchess/moves/MoveGen.hpp b/libchess/include/libchess/moves/MoveGen.hpp index 9344545e..89d1ab89 100644 --- a/libchess/include/libchess/moves/MoveGen.hpp +++ b/libchess/include/libchess/moves/MoveGen.hpp @@ -314,7 +314,7 @@ namespace detail { }) | std::ranges::to(); }) - .value_or(EPMoves {}); + .value_or(EPMoves { }); } template @@ -592,20 +592,20 @@ namespace detail { // castling out of check is not allowed if (position.is_check()) - return Moves {}; + return Moves { }; beman::inplace_vector::inplace_vector moves; get_kingside_castling(position, allOccupied) .transform([&moves](const Move move) { moves.emplace_back(move); - return std::monostate {}; + return std::monostate { }; }); get_queenside_castling(position, allOccupied) .transform([&moves](const Move move) { moves.emplace_back(move); - return std::monostate {}; + return std::monostate { }; }); return moves; diff --git a/libchess/include/libchess/moves/Perft.hpp b/libchess/include/libchess/moves/Perft.hpp index 4779e83d..b51cb4d5 100644 --- a/libchess/include/libchess/moves/Perft.hpp +++ b/libchess/include/libchess/moves/Perft.hpp @@ -131,7 +131,7 @@ auto perft( // NOLINT(readability-function-cognitive-complexity) .checks = 0uz, .checkmates = 0uz, .stalemates = 0uz, - .rootNodes = {} + .rootNodes = { } }; PerftResult result; diff --git a/libchess/include/libchess/uci/EngineBase.hpp b/libchess/include/libchess/uci/EngineBase.hpp index 1daad3d9..26ac953c 100644 --- a/libchess/include/libchess/uci/EngineBase.hpp +++ b/libchess/include/libchess/uci/EngineBase.hpp @@ -68,7 +68,7 @@ struct EngineBase { [[nodiscard]] virtual auto get_author() const -> string_view = 0; /** This must return the list of all options the engine supports. */ - [[nodiscard]] virtual auto get_options() -> std::span { return {}; } + [[nodiscard]] virtual auto get_options() -> std::span { return { }; } /** This function will be called when the "isready" command is received, and may block while waiting for background tasks to complete. This diff --git a/libchess/include/libchess/uci/Options.hpp b/libchess/include/libchess/uci/Options.hpp index afa63479..8f0579bc 100644 --- a/libchess/include/libchess/uci/Options.hpp +++ b/libchess/include/libchess/uci/Options.hpp @@ -251,14 +251,14 @@ struct StringOption final : Option { [[nodiscard]] auto get_value() const noexcept -> string_view { if (value == "") - return {}; + return { }; return value; } [[nodiscard]] auto get_value_variant() const -> Variant override { return get_value(); } - [[nodiscard]] auto get_default_value_variant() const -> Variant override { return string_view {}; } + [[nodiscard]] auto get_default_value_variant() const -> Variant override { return string_view { }; } [[nodiscard]] auto get_name() const noexcept -> string_view override { return optionName; } diff --git a/libchess/include/libchess/uci/Printing.hpp b/libchess/include/libchess/uci/Printing.hpp index d6c202b1..3404ea80 100644 --- a/libchess/include/libchess/uci/Printing.hpp +++ b/libchess/include/libchess/uci/Printing.hpp @@ -79,6 +79,11 @@ struct SearchInfo final { The value should be negative if the engine is getting mated. */ int plies { 0 }; + + /** Returns the number of full moves to mate. + The value is negative if the engine is getting mated. + */ + [[nodiscard]] auto moves() const noexcept -> int; }; /** The evaluation value. */ @@ -119,6 +124,9 @@ struct SearchInfo final { with the rest of the search information. */ std::string extraInformation; + + /** Returns the nodes-per-second for this search. */ + [[nodiscard]] auto get_nps() const noexcept -> size_t; }; /** Prints a UCI-formatted search info string to standard output. diff --git a/libchess/include/libchess/util/Strings.hpp b/libchess/include/libchess/util/Strings.hpp index 7e68c874..204916b2 100644 --- a/libchess/include/libchess/util/Strings.hpp +++ b/libchess/include/libchess/util/Strings.hpp @@ -166,7 +166,7 @@ void write_integer( const std::integral auto value, std::string& output) { - std::array buffer {}; + std::array buffer { }; const auto result = std::to_chars( buffer.data(), @@ -174,7 +174,7 @@ void write_integer( value); // simply do nothing on failure - if (result.ec != std::errc {}) + if (result.ec != std::errc { }) return; output.append( diff --git a/libchess/include/libchess/util/TextTable.hpp b/libchess/include/libchess/util/TextTable.hpp index 48cb1847..2646e5ce 100644 --- a/libchess/include/libchess/util/TextTable.hpp +++ b/libchess/include/libchess/util/TextTable.hpp @@ -20,6 +20,7 @@ #pragma once #include // IWYU pragma: keep - for size_t +#include #include #include #include @@ -49,6 +50,32 @@ struct [[nodiscard]] TextTable final { /** Concatenates all the rows in the table into a single string, with separators between rows and columns. */ [[nodiscard]] auto to_string() const -> string; + /** Typedef for a function that accepts a string as an argument. */ + using PrintFunc = std::function; + + /** Prints the table by calling lambda functions for each different element of the table. + This allows you to do things like print the table to a terminal using ANSI color codes; using + just the ``to_string()`` API would make it much more difficult to intersperse the needed escape + codes to switch between styles for the cell text and outlines, etc. + + @param printHeading Function object that will be called to print the contents of each heading cell. + Note that the strings sent to this function will include trailing whitespace, to create the table's alignment. + + @param printCell Function object that will be called to print the contents of each non-heading cell. + Note that the strings sent to this function will include trailing whitespace, to create the table's alignment. + + @param printOutline Function object that will be called to print outline/border characters between cells + and surrounding the entire table. + + @param printNewline Function object that accepts no arguments and should insert a single newline character + into the text stream being constructed. + */ + void print( + PrintFunc&& printHeading, + PrintFunc&& printCell, + PrintFunc&& printOutline, + std::function printNewline) const; + private: struct Row final { void add_column(string_view text) { columns.emplace_back(text); } @@ -60,6 +87,11 @@ struct [[nodiscard]] TextTable final { [[nodiscard]] auto to_string(std::span widths) const -> string; + void print( + PrintFunc printCell, + PrintFunc printOutline, + std::span widths) const; + private: std::vector columns; }; diff --git a/libchess/src/BoardPrinting.cpp b/libchess/src/BoardPrinting.cpp index ca2914c7..05db16bd 100644 --- a/libchess/src/BoardPrinting.cpp +++ b/libchess/src/BoardPrinting.cpp @@ -53,7 +53,7 @@ namespace { // and must return the text to go inside that square, or // a space if it's empty template - [[nodiscard, gnu::cold]] auto generate_board_string(Func getSquareText) -> string + [[nodiscard]] auto generate_board_string(Func getSquareText) -> string { static constexpr bool FuncReturnsChar = std::is_same_v< std::invoke_result_t, diff --git a/libchess/src/game/Zobrist.cpp b/libchess/src/game/Zobrist.cpp index 185b46e8..2c2852ff 100644 --- a/libchess/src/game/Zobrist.cpp +++ b/libchess/src/game/Zobrist.cpp @@ -69,7 +69,7 @@ namespace { return std::transform_reduce( squares.begin(), squares.end(), prevValue, - std::bit_xor<> {}, + std::bit_xor<> { }, [type](const Square square) { return piece_key(type, Side, square); }); diff --git a/libchess/src/moves/Magics.cpp b/libchess/src/moves/Magics.cpp index f5cf4533..e7e08c6d 100644 --- a/libchess/src/moves/Magics.cpp +++ b/libchess/src/moves/Magics.cpp @@ -43,7 +43,7 @@ namespace { [[nodiscard]] consteval auto calculate_bishop_masks() -> MaskArray { - MaskArray result {}; + MaskArray result { }; std::ranges::transform( masks::ALL.subboards(), @@ -60,7 +60,7 @@ namespace { using board::File; using board::Rank; - MaskArray result {}; + MaskArray result { }; for (const auto square : masks::ALL.squares()) { auto& value = result.at(square.index()); @@ -153,7 +153,7 @@ namespace { value = pseudo_legal::bishop( Bitboard::from_square(square), - occupied.inverse(), {}); + occupied.inverse(), { }); occupied = permute(BISHOP_MASKS.at(i), occupied); } while (occupied.any()); @@ -168,7 +168,7 @@ namespace { value = pseudo_legal::rook( Bitboard::from_square(square), - occupied.inverse(), {}); + occupied.inverse(), { }); occupied = permute(ROOK_MASKS.at(i), occupied); } while (occupied.any()); diff --git a/libchess/src/notation/Algebraic.cpp b/libchess/src/notation/Algebraic.cpp index 59a51305..0f5a03af 100644 --- a/libchess/src/notation/Algebraic.cpp +++ b/libchess/src/notation/Algebraic.cpp @@ -66,7 +66,7 @@ namespace { const auto newPos = after_move(position, move); if (not newPos.is_check()) - return {}; + return { }; if (not moves::any_legal_moves(newPos)) return "#"; // checkmate @@ -80,7 +80,7 @@ namespace { const auto pieceMoves = get_possible_move_origins(position, move.to(), move.piece()); if (std::cmp_less(pieceMoves.size(), 2)) - return {}; + return { }; // Order of preference for disambiguation: // 1. file of departure if different diff --git a/libchess/src/notation/EPD.cpp b/libchess/src/notation/EPD.cpp index 5b79c4a6..d4df709d 100644 --- a/libchess/src/notation/EPD.cpp +++ b/libchess/src/notation/EPD.cpp @@ -81,7 +81,7 @@ auto from_epd(string_view epdString) -> PositionOrError EPDPosition pos { .position = Position::empty(), - .operations = {} + .operations = { } }; const auto [piecePositions, rest1] = split_at_first_space(epdString); diff --git a/libchess/src/notation/FENHelpers.cpp b/libchess/src/notation/FENHelpers.cpp index e074a550..be585864 100644 --- a/libchess/src/notation/FENHelpers.cpp +++ b/libchess/src/notation/FENHelpers.cpp @@ -129,12 +129,12 @@ void write_en_passant_target_square( output.push_back(file_to_char(epSquare.file)); output.push_back(rank_to_char(epSquare.rank)); - return std::monostate {}; + return std::monostate { }; }) .or_else([&output] { output.push_back('-'); - return std::make_optional(std::monostate {}); + return std::make_optional(std::monostate { }); }); } @@ -219,7 +219,7 @@ auto parse_piece_positions( position.whitePieces.refresh_occupied(); position.blackPieces.refresh_occupied(); - return {}; + return { }; } auto parse_side_to_move( @@ -238,7 +238,7 @@ auto parse_side_to_move( position.sideToMove = isBlack ? Color::Black : Color::White; - return {}; + return { }; } void parse_castling_rights( diff --git a/libchess/src/notation/PGN.cpp b/libchess/src/notation/PGN.cpp index 5141fbb9..f979f613 100644 --- a/libchess/src/notation/PGN.cpp +++ b/libchess/src/notation/PGN.cpp @@ -138,7 +138,7 @@ namespace { if (not output.empty()) output.back().comment = trim(pgnText.substr(1uz)); - return {}; + return { }; } if (not output.empty()) @@ -210,7 +210,7 @@ namespace { pgnText = trim(pgnText); if (pgnText.empty()) - return {}; + return { }; switch (pgnText.front()) { case '{': { @@ -348,7 +348,7 @@ auto from_pgn(const string_view pgnText) -> GameOrError .and_then([&game](const string_view afterMeta) -> GameOrError { if (const auto posStr = game.metadata.find("FEN"); posStr != game.metadata.end()) { - game.startingPosition = from_fen(posStr->second).value_or(Position {}); + game.startingPosition = from_fen(posStr->second).value_or(Position { }); } return parse_move_list(afterMeta, game.startingPosition, game.moves) @@ -467,7 +467,7 @@ namespace { write_metadata_item(key, value, output); } - static const Position startPos {}; + static const Position startPos { }; if (startingPosition != startPos) { if (not metadata.contains("FEN"s)) { @@ -559,7 +559,7 @@ namespace { default: std::unreachable(); } - return std::monostate {}; + return std::monostate { }; }); } diff --git a/libchess/src/notation/UCI.cpp b/libchess/src/notation/UCI.cpp index e0a0cd98..ef29efe2 100644 --- a/libchess/src/notation/UCI.cpp +++ b/libchess/src/notation/UCI.cpp @@ -68,7 +68,7 @@ auto from_uci( return std::unexpected { "Cannot parse UCI move from empty string" }; if (text == UCI_NULL_MOVE) - return Move {}; + return Move { }; return Square::from_string(text.substr(0uz, 2uz)) .and_then([&text, &position](const Square from) { diff --git a/libchess/src/uci/CommandParsing.cpp b/libchess/src/uci/CommandParsing.cpp index 4dc0d73b..72f24c4d 100644 --- a/libchess/src/uci/CommandParsing.cpp +++ b/libchess/src/uci/CommandParsing.cpp @@ -50,7 +50,7 @@ auto parse_position_options(string_view options) options = trim(options); - Position position {}; + Position position { }; auto [secondWord, rest] = split_at_first_space(options); diff --git a/libchess/src/uci/EngineBase.cpp b/libchess/src/uci/EngineBase.cpp index 38909c0a..41b2ae0f 100644 --- a/libchess/src/uci/EngineBase.cpp +++ b/libchess/src/uci/EngineBase.cpp @@ -165,13 +165,13 @@ void EngineBase::handle_setpos(const string_view arguments) set_position(pos); - return {}; + return { }; }) .transform_error([this](const string_view error) { info_string(std::format("Error setting position: {}", error)); info_string(std::format("Retained previous position: {}", notation::to_fen(position))); - return std::monostate {}; + return std::monostate { }; }); } @@ -209,7 +209,7 @@ void EngineBase::handle_setoption(const string_view arguments) auto* option = *it; if (isNPos) - option->handle_setvalue({}); + option->handle_setvalue({ }); else option->handle_setvalue(trim(rest.substr(valueTokenIdx))); } else { diff --git a/libchess/src/uci/Printing.cpp b/libchess/src/uci/Printing.cpp index bd0f2aef..799642bb 100644 --- a/libchess/src/uci/Printing.cpp +++ b/libchess/src/uci/Printing.cpp @@ -12,6 +12,7 @@ * ====================================================================================== */ +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +46,7 @@ std::monostate info_string(const string_view info) { println(cout, "info string {}", info); - return std::monostate {}; + return std::monostate { }; } namespace { @@ -56,7 +58,7 @@ namespace { .transform([](const Move move) { return std::format(" ponder {}", to_uci(move)); }) - .value_or(string {}); + .value_or(string { }); } } // namespace @@ -70,15 +72,31 @@ void best_move( cout.flush(); } -namespace { - [[nodiscard, gnu::const]] auto plies_to_moves(int plies) noexcept -> int - { - if (std::cmp_greater(plies, 0)) - ++plies; +auto SearchInfo::Score::MateIn::moves() const noexcept -> int +{ + return [ply = plies]() mutable { + if (std::cmp_greater(ply, 0)) + ++ply; - return plies / 2; - } + return ply / 2; + }(); +} + +auto SearchInfo::get_nps() const noexcept -> size_t +{ + using FractionalSeconds = std::chrono::duration; + + const auto seconds = duration_cast(time).count(); + + if (seconds <= 0.) + return 0uz; + + const auto nps = static_cast(nodes) / seconds; + return static_cast(std::round(nps)); +} + +namespace { using Score = SearchInfo::Score; [[nodiscard]] auto base_score_string(const Score& score) -> string @@ -86,7 +104,7 @@ namespace { return std::visit( util::Visitor { [](const Score::Centipawns& centipawns) { return std::format("cp {}", centipawns.value); }, - [](const Score::MateIn& mate) { return std::format("mate {}", plies_to_moves(mate.plies)); } }, + [](const Score::MateIn& mate) { return std::format("mate {}", mate.moves()); } }, score.value); } @@ -102,11 +120,11 @@ namespace { return string; } - [[nodiscard]] auto pv_string(const moves::MoveList& pv) -> string + [[nodiscard]] auto pv_string(const std::span pv) -> string { if (pv.empty()) { // this is possible if we're checkmated - return {}; + return { }; } string result { " pv " }; @@ -121,22 +139,10 @@ namespace { return result; } - [[nodiscard, gnu::const]] auto get_nodes_per_second(const SearchInfo& info) -> size_t - { - const auto seconds = static_cast(info.time.count()) * 0.001; - - if (seconds <= 0.) - return 0uz; - - const auto nps = static_cast(info.nodes) / seconds; - - return static_cast(std::round(nps)); - } - [[nodiscard]] auto get_extra_info_string(const string_view info) -> string { if (info.empty()) - return {}; + return { }; return std::format(" string {}", info); } @@ -149,7 +155,7 @@ void search_info(const SearchInfo& info) info.depth, score_string(info.score), info.time.count(), info.hashfull, info.nodes, - get_nodes_per_second(info), + info.get_nps(), info.selDepth, info.tbHits, pv_string(info.pv), get_extra_info_string(info.extraInformation)); diff --git a/libchess/src/util/Files.cpp b/libchess/src/util/Files.cpp index 0773f12f..fbb689df 100644 --- a/libchess/src/util/Files.cpp +++ b/libchess/src/util/Files.cpp @@ -44,7 +44,7 @@ auto load_file_as_string( using Iterator = std::istreambuf_iterator; - return string { Iterator { input }, Iterator {} }; + return string { Iterator { input }, Iterator { } }; } catch (const std::exception& exception) { return std::unexpected { std::format( "Error while reading file at path '{}': {}", diff --git a/libchess/src/util/Logger.cpp b/libchess/src/util/Logger.cpp index d0880c5b..59af4486 100644 --- a/libchess/src/util/Logger.cpp +++ b/libchess/src/util/Logger.cpp @@ -133,7 +133,7 @@ auto Logger::start(const path& logFile) -> MaybeError logger.close_log_file(); if (logFile.empty()) - return {}; + return { }; logger.file.open(logFile, std::ios_base::out); @@ -148,7 +148,7 @@ auto Logger::start(const path& logFile) -> MaybeError std::cin.rdbuf(&logger.in); std::cout.rdbuf(&logger.out); - return {}; + return { }; } void Logger::close_log_file() diff --git a/libchess/src/util/Strings.cpp b/libchess/src/util/Strings.cpp index 67a35c80..917a6ff9 100644 --- a/libchess/src/util/Strings.cpp +++ b/libchess/src/util/Strings.cpp @@ -103,7 +103,7 @@ auto split_at_first_space(const string_view input) -> StringViewPair const auto spaceIdx = input.find(' '); if (spaceIdx == string_view::npos) - return { input, {} }; + return { input, { } }; return { input.substr(0uz, spaceIdx), @@ -118,7 +118,7 @@ auto split_at_first_space_or_newline(const string_view input) -> StringViewPair input.find('\n')); if (firstDelimIdx == string_view::npos) - return { input, {} }; + return { input, { } }; return { input.substr(0uz, firstDelimIdx), diff --git a/libchess/src/util/TextTable.cpp b/libchess/src/util/TextTable.cpp index b24798d9..a1fab6e4 100644 --- a/libchess/src/util/TextTable.cpp +++ b/libchess/src/util/TextTable.cpp @@ -14,11 +14,13 @@ #include #include // IWYU pragma: keep - for size_t; +#include #include #include #include #include #include +#include #include namespace chess::util::strings { @@ -88,6 +90,29 @@ auto TextTable::to_string() const -> string return result; } +void TextTable::print( + PrintFunc&& printHeading, + PrintFunc&& printCell, + PrintFunc&& printOutline, + std::function printNewline) const +{ + const auto widths = get_column_widths(); + + rows.front().print( + std::move(printHeading), printOutline, widths); + + printNewline(); + + printOutline(make_header_sep_row(widths)); + + printNewline(); + + for (const auto& row : rows | std::views::drop(1uz)) { + row.print(printCell, printOutline, widths); + printNewline(); + } +} + auto TextTable::num_columns() const -> size_t { return std::transform_reduce( @@ -142,4 +167,32 @@ auto TextTable::Row::to_string( return result; } +void TextTable::Row::print( + PrintFunc printCell, + PrintFunc printOutline, + std::span widths) const +{ + printOutline(LINE_START); + + auto index { 0uz }; + + for (const auto width : widths) { + if (index > 0uz) + printOutline(COLUMN_SEPARATOR); + + string padded; + + if (index < columns.size()) + padded = columns[index]; + + padded.resize(width, ' '); + + printCell(padded); + + ++index; + } + + printOutline(LINE_ENDING); +} + } // namespace chess::util::strings diff --git a/libchess/src/util/memory/PageAlignedAlloc_Windows.hpp b/libchess/src/util/memory/PageAlignedAlloc_Windows.hpp index 0a8c1e1d..4a6c6661 100644 --- a/libchess/src/util/memory/PageAlignedAlloc_Windows.hpp +++ b/libchess/src/util/memory/PageAlignedAlloc_Windows.hpp @@ -86,19 +86,19 @@ namespace impl { // We need SeLockMemoryPrivilege, so try to enable it for the process - HANDLE hProcessToken {}; + HANDLE hProcessToken { }; if (! OpenProcessToken_f( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken)) { return nullptr; } - LUID luid {}; + LUID luid { }; void* mem { nullptr }; if (LookupPrivilegeValueA_f(nullptr, "SeLockMemoryPrivilege", &luid)) { - TOKEN_PRIVILEGES tp {}; // NOLINT(readability-identifier-length) - TOKEN_PRIVILEGES prevTp {}; + TOKEN_PRIVILEGES tp { }; // NOLINT(readability-identifier-length) + TOKEN_PRIVILEGES prevTp { }; DWORD prevTpLen = 0; tp.PrivilegeCount = 1; diff --git a/tests/rampart/main.cpp b/tests/rampart/main.cpp index 68342ce8..a6d9fe2d 100644 --- a/tests/rampart/main.cpp +++ b/tests/rampart/main.cpp @@ -102,7 +102,7 @@ try { }) .transform_error([](const std::string_view error) { std::println(std::cerr, "Error: {}", error); - return std::monostate {}; + return std::monostate { }; }); return EXIT_SUCCESS; diff --git a/tests/unit/libbenbot/data-structures/TranspositionTable.cpp b/tests/unit/libbenbot/data-structures/TranspositionTable.cpp index cfd3374c..578b5c89 100644 --- a/tests/unit/libbenbot/data-structures/TranspositionTable.cpp +++ b/tests/unit/libbenbot/data-structures/TranspositionTable.cpp @@ -28,7 +28,7 @@ using EvalType = ben_bot::EvalType; namespace notation = chess::notation; -static const Position startPos {}; +static const Position startPos { }; TEST_CASE("Transposition table - find()", TAGS) { @@ -36,7 +36,7 @@ TEST_CASE("Transposition table - find()", TAGS) .searchedDepth = 1uz, .eval = 0, .evalType = EvalType::Exact, - .bestMove = {} + .bestMove = { } }; const auto pos2 = notation::from_fen("8/8/4n3/2B1k1p1/3Pn3/2K5/5R2/8 b - - 0 1") @@ -118,7 +118,7 @@ TEST_CASE("Transposition table - probe_eval()", TAGS) { .searchedDepth = DEPTH, .eval = EVAL, .evalType = EvalType::Exact, - .bestMove = {} }); + .bestMove = { } }); const auto probed = table.probe_eval(startPos, DEPTH, BOUNDS); @@ -136,7 +136,7 @@ TEST_CASE("Transposition table - probe_eval()", TAGS) { .searchedDepth = DEPTH, .eval = EVAL, .evalType = EvalType::Alpha, - .bestMove = {} }); + .bestMove = { } }); SECTION("Probing with alpha that doesn't cutoff") { @@ -170,7 +170,7 @@ TEST_CASE("Transposition table - probe_eval()", TAGS) { .searchedDepth = DEPTH, .eval = EVAL, .evalType = EvalType::Beta, - .bestMove = {} }); + .bestMove = { } }); SECTION("Probing with beta that doesn't cutoff") { @@ -207,7 +207,7 @@ TEST_CASE("Transposition table - store() overwriting rules", TAGS) .searchedDepth = 6uz, .eval = 2, .evalType = EvalType::Exact, - .bestMove = {} + .bestMove = { } }; table.store(startPos, oldRecord); @@ -220,7 +220,7 @@ TEST_CASE("Transposition table - store() overwriting rules", TAGS) .searchedDepth = oldRecord.searchedDepth - 1uz, .eval = 4, .evalType = EvalType::Exact, - .bestMove = {} + .bestMove = { } }; table.store(startPos, newRecord); @@ -236,7 +236,7 @@ TEST_CASE("Transposition table - store() overwriting rules", TAGS) .searchedDepth = oldRecord.searchedDepth, .eval = -6, .evalType = EvalType::Alpha, - .bestMove = {} + .bestMove = { } }; table.store(startPos, newRecord); @@ -250,7 +250,7 @@ TEST_CASE("Transposition table - store() overwriting rules", TAGS) .searchedDepth = oldRecord.searchedDepth, .eval = 6, .evalType = EvalType::Beta, - .bestMove = {} + .bestMove = { } }; table.store(startPos, newRecord); diff --git a/tests/unit/libbenbot/eval/Score.cpp b/tests/unit/libbenbot/eval/Score.cpp index fbd68be0..07a49842 100644 --- a/tests/unit/libbenbot/eval/Score.cpp +++ b/tests/unit/libbenbot/eval/Score.cpp @@ -21,7 +21,7 @@ using ben_bot::eval::Score; TEST_CASE("Score - is_mate()", TAGS) { - REQUIRE_FALSE(Score {}.is_mate()); + REQUIRE_FALSE(Score { }.is_mate()); static constexpr auto mate = Score::mate(0uz); diff --git a/tests/unit/libchess/game/Position.cpp b/tests/unit/libchess/game/Position.cpp index 38e76b01..529c8901 100644 --- a/tests/unit/libchess/game/Position.cpp +++ b/tests/unit/libchess/game/Position.cpp @@ -36,7 +36,7 @@ using chess::notation::from_fen; TEST_CASE("Position - starting", TAGS) { - const Position pos {}; + const Position pos { }; REQUIRE(pos.sideToMove == Color::White); REQUIRE_FALSE(pos.enPassantTargetSquare); @@ -115,7 +115,7 @@ TEST_CASE("Position - is_check()", TAGS) { SECTION("Starting position") { - const Position startingPosition {}; + const Position startingPosition { }; REQUIRE_FALSE(startingPosition.is_check()); REQUIRE_FALSE(startingPosition.is_checkmate()); @@ -248,7 +248,7 @@ TEST_CASE("Position - passed pawns", TAGS) { SECTION("Starting position") { - const Position startingPosition {}; + const Position startingPosition { }; REQUIRE(startingPosition.get_passed_pawns().none()); REQUIRE(startingPosition.get_passed_pawns().none()); @@ -277,7 +277,7 @@ TEST_CASE("Position - backward pawns", TAGS) { SECTION("Starting position") { - const Position startingPosition {}; + const Position startingPosition { }; REQUIRE(startingPosition.get_backward_pawns().none()); REQUIRE(startingPosition.get_backward_pawns().none()); @@ -307,7 +307,7 @@ TEST_CASE("Position - backward pawns", TAGS) TEST_CASE("Position - null move", TAGS) { - const Position startingPosition {}; + const Position startingPosition { }; const auto afterNull = after_null_move(startingPosition); @@ -316,7 +316,7 @@ TEST_CASE("Position - null move", TAGS) TEST_CASE("Position - fifty-move draws", TAGS) { - REQUIRE(! Position {}.is_fifty_move_draw()); + REQUIRE(! Position { }.is_fifty_move_draw()); REQUIRE(from_fen("7k/4NK2/5r2/5BN1/8/8/8/8 w - - 103 115").value().is_fifty_move_draw()); REQUIRE(from_fen("8/7k/8/1r3KR1/5B2/8/8/8 w - - 105 122").value().is_fifty_move_draw()); diff --git a/tests/unit/libchess/game/ThreefoldRepetition.cpp b/tests/unit/libchess/game/ThreefoldRepetition.cpp index 0e2b86c1..09dbf23e 100644 --- a/tests/unit/libchess/game/ThreefoldRepetition.cpp +++ b/tests/unit/libchess/game/ThreefoldRepetition.cpp @@ -23,7 +23,7 @@ using chess::notation::from_alg; TEST_CASE("Position - threefold repetitions", TAGS) { - chess::game::Position pos {}; + chess::game::Position pos { }; REQUIRE_FALSE(pos.is_threefold_repetition()); @@ -58,7 +58,7 @@ TEST_CASE("Position - threefold repetitions", TAGS) TEST_CASE("Threefold repetition from differing moves", TAGS) { - chess::game::Position pos {}; + chess::game::Position pos { }; pos.make_move(from_alg(pos, "Nf3").value()); pos.make_move(from_alg(pos, "Nf6").value()); diff --git a/tests/unit/libchess/game/ZobristHashing.cpp b/tests/unit/libchess/game/ZobristHashing.cpp index 5cfe7490..2ce68035 100644 --- a/tests/unit/libchess/game/ZobristHashing.cpp +++ b/tests/unit/libchess/game/ZobristHashing.cpp @@ -27,7 +27,7 @@ using chess::notation::from_fen; TEST_CASE("Zobrist - starting position", TAGS) { - const chess::game::Position startPosition {}; + const chess::game::Position startPosition { }; const auto startPos = from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") .value(); @@ -40,7 +40,7 @@ TEST_CASE("Zobrist - reaching identical positions", TAGS) const auto position = from_fen("rnbqkbnr/ppp1pppp/8/3P4/8/8/PPPP1PPP/RNBQKBNR b KQkq - 0 2") .value(); - chess::game::Position pos {}; + chess::game::Position pos { }; pos.make_move(from_alg(pos, "e4").value()); pos.make_move(from_alg(pos, "d5").value()); @@ -55,7 +55,7 @@ TEST_CASE("Zobrist - hash changes", TAGS) SECTION("From start pos") { - const chess::game::Position pos {}; + const chess::game::Position pos { }; const auto oldHash = pos.hash; @@ -102,7 +102,7 @@ TEST_CASE("Zobrist - loading identical FENs", TAGS) TEST_CASE("Zobrist - repeated positions", TAGS) { - chess::game::Position pos {}; + chess::game::Position pos { }; const auto origHash = pos.hash; diff --git a/tests/unit/libchess/moves/PseudoLegal.cpp b/tests/unit/libchess/moves/PseudoLegal.cpp index beb50c70..a1b7fbed 100644 --- a/tests/unit/libchess/moves/PseudoLegal.cpp +++ b/tests/unit/libchess/moves/PseudoLegal.cpp @@ -126,7 +126,7 @@ TEST_CASE("Pseudo-legal - pawn double pushes", TAGS) static constexpr auto starting = starting_masks::white::PAWNS; static constexpr auto allPushes = move_gen::pawn_double_pushes( - starting, {}); + starting, { }); STATIC_REQUIRE(allPushes == board_masks::ranks::FOUR); @@ -153,7 +153,7 @@ TEST_CASE("Pseudo-legal - pawn double pushes", TAGS) static constexpr auto starting = starting_masks::black::PAWNS; static constexpr auto allPushes = move_gen::pawn_double_pushes( - starting, {}); + starting, { }); STATIC_REQUIRE(allPushes == board_masks::ranks::FIVE); @@ -357,7 +357,7 @@ TEST_CASE("Pseudo-legal - knights", TAGS) { static constexpr auto starting = Bitboard::from_square(Square { File::D, Rank::One }); - static constexpr auto allMoves = move_gen::knight(starting, {}); + static constexpr auto allMoves = move_gen::knight(starting, { }); STATIC_REQUIRE(allMoves.count() == 4uz); @@ -386,7 +386,7 @@ TEST_CASE("Pseudo-legal - knights", TAGS) starting.set(Square { File::E, Rank::Four }); starting.set(Square { File::D, Rank::Four }); - const auto allMoves = move_gen::knight(starting, {}); + const auto allMoves = move_gen::knight(starting, { }); REQUIRE(allMoves.count() == 16uz); diff --git a/tests/unit/libchess/notation/Algebraic.cpp b/tests/unit/libchess/notation/Algebraic.cpp index 4dd0fcf5..d1b56da8 100644 --- a/tests/unit/libchess/notation/Algebraic.cpp +++ b/tests/unit/libchess/notation/Algebraic.cpp @@ -39,7 +39,7 @@ TEST_CASE("Algebraic notation - piece moves", TAGS) { SECTION("Knights") { - Position position {}; + Position position { }; { const auto move = from_alg(position, "Nc3").value(); @@ -405,7 +405,7 @@ TEST_CASE("Algebraic notation - pawn pushes", TAGS) { SECTION("Normal") { - const Position startingPosition {}; + const Position startingPosition { }; const auto move = from_alg(startingPosition, "e3").value(); @@ -457,7 +457,7 @@ TEST_CASE("Algebraic notation - pawn double pushes", TAGS) { SECTION("Normal") { - Position startingPosition {}; + Position startingPosition { }; const auto move = from_alg(startingPosition, "e4").value(); diff --git a/tests/unit/libchess/notation/EPD.cpp b/tests/unit/libchess/notation/EPD.cpp index 37b808af..ccb35e69 100644 --- a/tests/unit/libchess/notation/EPD.cpp +++ b/tests/unit/libchess/notation/EPD.cpp @@ -23,7 +23,7 @@ using chess::notation::from_epd; TEST_CASE("EPD - start position", TAGS) { - const chess::game::Position startPos {}; + const chess::game::Position startPos { }; const auto epd = from_epd("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - hmvc 0; fmvn 1;") .value(); diff --git a/tests/unit/libchess/notation/UCI.cpp b/tests/unit/libchess/notation/UCI.cpp index 81c04223..391d644a 100644 --- a/tests/unit/libchess/notation/UCI.cpp +++ b/tests/unit/libchess/notation/UCI.cpp @@ -35,7 +35,7 @@ using chess::notation::to_uci; TEST_CASE("UCI notation - normal moves", TAGS) { - const chess::game::Position startingPosition {}; + const chess::game::Position startingPosition { }; SECTION("Pawn move") { @@ -238,11 +238,11 @@ TEST_CASE("UCI notation - promotions", TAGS) TEST_CASE("UCI notation - null moves", TAGS) { - const chess::game::Position position {}; + const chess::game::Position position { }; const auto move = from_uci(position, "0000").value(); REQUIRE(move.is_null()); - REQUIRE(to_uci(chess::moves::Move {}) == "0000"); + REQUIRE(to_uci(chess::moves::Move { }) == "0000"); } diff --git a/tests/unit/libchess/uci/CommandParsing.cpp b/tests/unit/libchess/uci/CommandParsing.cpp index 16ab77ad..6f3323ff 100644 --- a/tests/unit/libchess/uci/CommandParsing.cpp +++ b/tests/unit/libchess/uci/CommandParsing.cpp @@ -33,7 +33,7 @@ TEST_CASE("UCI parsing - position", TAGS) SECTION("From start position") { - const Position startPos {}; + const Position startPos { }; REQUIRE(parse_position_options(" startpos \n") == startPos); @@ -59,7 +59,7 @@ TEST_CASE("UCI parsing - go", TAGS) { using chess::uci::parse_go_options; - const Position startPos {}; + const Position startPos { }; SECTION("No arguments") { diff --git a/tests/unit/libchess/util/Strings.cpp b/tests/unit/libchess/util/Strings.cpp index 646c99e8..ba641a71 100644 --- a/tests/unit/libchess/util/Strings.cpp +++ b/tests/unit/libchess/util/Strings.cpp @@ -24,7 +24,7 @@ TEST_CASE("Strings - trim()", TAGS) { using chess::util::strings::trim; - REQUIRE(trim({}).empty()); + REQUIRE(trim({ }).empty()); REQUIRE(trim(" ").empty()); REQUIRE(trim("\n").empty()); @@ -44,7 +44,7 @@ TEST_CASE("Strings - split_at_first_space()", TAGS) SECTION("Empty string") { - const auto [before, after] = split_at_first_space({}); + const auto [before, after] = split_at_first_space({ }); REQUIRE(before.empty()); REQUIRE(after.empty()); @@ -89,7 +89,7 @@ TEST_CASE("Strings - split_at_first_space_or_newline()", TAGS) SECTION("Empty string") { - const auto [before, after] = split_at_first_space_or_newline({}); + const auto [before, after] = split_at_first_space_or_newline({ }); REQUIRE(before.empty()); REQUIRE(after.empty()); @@ -173,7 +173,7 @@ TEST_CASE("Strings - lines_view()", TAGS) | std::ranges::to(); }; - REQUIRE(lines_vector({}).empty()); + REQUIRE(lines_vector({ }).empty()); SECTION("Single line") { @@ -201,7 +201,7 @@ TEST_CASE("Strings - words_view()", TAGS) | std::ranges::to(); }; - REQUIRE(words_vector({}).empty()); + REQUIRE(words_vector({ }).empty()); SECTION("Single word") { diff --git a/tests/unit/libchess/util/TextTable.cpp b/tests/unit/libchess/util/TextTable.cpp new file mode 100644 index 00000000..01dae432 --- /dev/null +++ b/tests/unit/libchess/util/TextTable.cpp @@ -0,0 +1,64 @@ +/* + * ====================================================================================== + * + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓███████▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░▒▓████████▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓██████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ + * ░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒░ + * + * ====================================================================================== + */ + +#include +#include +#include +#include + +static constexpr auto TAGS { "[util][strings][TextTable]" }; + +using chess::util::strings::TextTable; + +namespace { +[[nodiscard]] auto get_printed_string(const TextTable& table) -> std::string +{ + std::string result; + + auto appendToString = [&result](const std::string_view toAdd) { + result.append(toAdd); + }; + + table.print( + appendToString, appendToString, appendToString, + [&result] { result.append(1uz, '\n'); }); + + return result; +} +} // namespace + +TEST_CASE("TextTable - to_string() should give same result as print()", TAGS) +{ + TextTable table; + + table.append_column("Option") + .append_column("Type") + .append_column("Notes") + .append_column("Default"); + + table.new_row() + .append_column("Option 1") + .append_column("Integer") + .append_column("Comments") + .append_column("0"); + + table.new_row() + .append_column("Option 2") + .append_column("String") + .append_column("More comments") + .append_column("foo"); + + REQUIRE( + get_printed_string(table) == table.to_string()); +} diff --git a/vcpkg.json b/vcpkg.json index 8ef9a39e..afae01d1 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -11,6 +11,10 @@ { "name": "nlohmann-json", "version>=": "3.12.0" + }, + { + "name": "termcolor", + "version>=": "2.1.0" } ], "name": "BenBot",