From 2ef2a29601d26389966bcb27c5de55e166a7e433 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Mon, 2 Mar 2026 05:26:57 +0100 Subject: [PATCH] squash merge efficient-puzzle into main --- CMakeLists.txt | 110 ++-- README.md | 10 +- default.puzzle | 48 +- flake.nix | 16 + include/input_handler.hpp | 2 +- include/puzzle.hpp | 607 ++++++++++++-------- include/state_manager.hpp | 36 +- include/user_interface.hpp | 4 +- include/util.hpp | 88 ++- src/input_handler.cpp | 38 +- src/main.cpp | 70 +-- src/octree.cpp | 38 +- src/puzzle.cpp | 1106 +++++++++++++++++++++++++++--------- src/state_manager.cpp | 18 +- src/user_interface.cpp | 316 +++++------ test/bits.cpp | 267 +++++++++ 16 files changed, 1915 insertions(+), 859 deletions(-) create mode 100644 test/bits.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d015f9..b6f9ebd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,20 +4,48 @@ project(MassSprings) set(CMAKE_CXX_STANDARD 26) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if(POLICY CMP0167) + cmake_policy(SET CMP0167 NEW) +endif() + +option(DISABLE_BACKWARD "Disable backward stacktrace printer" OFF) +option(DISABLE_TRACY "Disable the Tracy profiler client" ON) +option(DISABLE_TESTS "Disable building and running tests" ON) + +# Headers + Sources +set(SOURCES + src/backward.cpp + src/graph_distances.cpp + src/input_handler.cpp + src/mass_spring_system.cpp + src/octree.cpp + src/orbit_camera.cpp + src/renderer.cpp + src/state_manager.cpp + src/threaded_physics.cpp + src/user_interface.cpp + src/puzzle.cpp +) + # Libraries find_package(raylib REQUIRED) -set(LIBS raylib) +find_package(Boost REQUIRED) +set(LIBS raylib Boost::headers) set(FLAGS "") -if(NOT DEFINED DISABLE_BACKWARD) +if(WIN32) + list(APPEND LIBS opengl32 gdi32 winmm) +endif() + +include(FetchContent) +if(NOT DISABLE_BACKWARD) find_package(Backward REQUIRED) list(APPEND LIBS Backward::Backward) list(APPEND FLAGS BACKWARD) endif() -if(NOT DEFINED DISABLE_TRACY) - include(FetchContent) +if(NOT DISABLE_TRACY) FetchContent_Declare(tracy GIT_REPOSITORY https://github.com/wolfpld/tracy.git GIT_TAG v0.11.1 @@ -32,14 +60,10 @@ if(NOT DEFINED DISABLE_TRACY) list(APPEND FLAGS TRACY) endif() -if(WIN32) - list(APPEND LIBS opengl32 gdi32 winmm) -endif() - # Set this after fetching tracy to hide tracy's warnings set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wcast-align -Wno-unused-parameter -Wunreachable-code") -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O2 -ggdb") # -fsanitize=address already fails on InitWindow(), -fsanitize=undefined, -fsanitize=leak -set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Ofast") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb -O0") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -ggdb -Ofast -march=native") message("-- CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}") message("-- CMAKE_C_FLAGS_DEBUG: ${CMAKE_C_FLAGS_DEBUG}") @@ -48,40 +72,42 @@ message("-- CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") message("-- CMAKE_CXX_FLAGS_DEBUG: ${CMAKE_CXX_FLAGS_DEBUG}") message("-- CMAKE_CXX_FLAGS_RELEASE: ${CMAKE_CXX_FLAGS_RELEASE}") -# Headers + Sources -include_directories(include) -set(SOURCES - src/backward.cpp - src/graph_distances.cpp - src/input_handler.cpp - src/main.cpp - src/mass_spring_system.cpp - src/octree.cpp - src/orbit_camera.cpp - src/threaded_physics.cpp - src/puzzle.cpp - src/renderer.cpp - src/state_manager.cpp - src/user_interface.cpp -) - # Main target -add_executable(masssprings ${SOURCES}) -target_include_directories(masssprings PRIVATE ${RAYLIB_CPP_INCLUDE_DIR}) +add_executable(masssprings src/main.cpp ${SOURCES}) +target_include_directories(masssprings PRIVATE include) target_link_libraries(masssprings PRIVATE ${LIBS}) target_compile_definitions(masssprings PRIVATE ${FLAGS}) +# Testing sources +if(NOT DISABLE_TESTS AND NOT WIN32) + enable_testing() + + FetchContent_Declare(Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.13.0 + ) + FetchContent_MakeAvailable(Catch2) + + set(TEST_SOURCES + test/bits.cpp + ) + + add_executable(tests ${TEST_SOURCES} ${SOURCES}) + target_include_directories(tests PRIVATE include) + target_link_libraries(tests Catch2::Catch2WithMain raylib) + + include(Catch) + catch_discover_tests(tests) +endif() + # LTO -if(NOT WIN32) - include(CheckIPOSupported) - check_ipo_supported(RESULT supported OUTPUT error) - if(supported) - message(STATUS "IPO / LTO enabled") - set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) - if(USE_TRACY) - set_property(TARGET masssprings_tracy PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) - endif() - else() - message(STATUS "IPO / LTO not supported: <${error}>") - endif() -endif() \ No newline at end of file +#if(NOT WIN32) +include(CheckIPOSupported) +check_ipo_supported(RESULT supported OUTPUT error) +if(supported) + message(STATUS "IPO / LTO enabled") + set_property(TARGET masssprings PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +else() + message(STATUS "IPO / LTO not supported") +endif() +#endif() \ No newline at end of file diff --git a/README.md b/README.md index 3abc96e..cdde540 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,12 @@ The graph layout is calculated iteratively using a mass-spring-system with addit ![](screenshot.png) -Build and run on NixOS: `nix run git+https://gitea.local.chriphost.de/christoph/cpp-masssprings`. \ No newline at end of file +## Running + +Requirements: + +- Directory `fonts` +- Directory `shader` +- Preset file `default.puzzle` (optional) + +Run `nix run git+https://gitea.local.chriphost.de/christoph/cpp-masssprings` from the working directory containing the listed requirements. \ No newline at end of file diff --git a/default.puzzle b/default.puzzle index 1796baa..b16c4fa 100644 --- a/default.puzzle +++ b/default.puzzle @@ -1,44 +1,30 @@ # RushHour 1 -R664231........13................12ba..........1221..12..12..21........31.... - +S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ _ _ 1x3} {_ _ _ _ _ _} {_ _ 1x2 2X1 _ _} {_ _ _ 1x2 2x1 _} {1x2 _ 1x2 _ 2x1 _} {_ _ _ 3x1 _ _}] # RushHour 2 -R66421231....1213..31........ba..121212..21................1221..........21.. - +S:[6x6] G:[4,2] M:[R] B:[{1x2 3x1 _ _ 1x2 1x3} {_ 3x1 _ _ _ _} {2X1 _ 1x2 1x2 1x2 _} {2x1 _ _ _ _ _} {_ _ _ 1x2 2x1 _} {_ _ _ _ 2x1 _}] # RushHour 3 -R664231....12....1221......12..ba..1212..21..12....12......21......21..21.... - +S:[6x6] G:[4,2] M:[R] B:[{3x1 _ _ 1x2 _ _} {1x2 2x1 _ _ _ 1x2} {_ 2X1 _ 1x2 1x2 _} {2x1 _ 1x2 _ _ 1x2} {_ _ _ 2x1 _ _} {_ 2x1 _ 2x1 _ _}] # RushHour 4 -R66421321....12....1212....13......ba....31....12........12..21..21....21.... - +S:[6x6] G:[4,2] M:[R] B:[{1x3 2x1 _ _ 1x2 _} {_ 1x2 1x2 _ _ 1x3} {_ _ _ 2X1 _ _} {3x1 _ _ 1x2 _ _} {_ _ 1x2 _ 2x1 _} {2x1 _ _ 2x1 _ _}] # RushHour + Walls 1 -R66421221..AA..........1221..12ba............1221..1321..........21..31...... - +S:[6x6] G:[4,2] M:[R] B:[{1x2 2x1 _ 1*1 _ _} {_ _ _ 1x2 2x1 _} {1x2 2X1 _ _ _ _} {_ _ 1x2 2x1 _ 1x3} {2x1 _ _ _ _ _} {2x1 _ 3x1 _ _ _}] # RushHour + Walls 2 -R664221....1212AA31..........12ba..12........12..21........21..12....21..AA.. - +S:[6x6] G:[4,2] M:[R] B:[{2x1 _ _ 1x2 1x2 1*1} {3x1 _ _ _ _ _} {1x2 2X1 _ 1x2 _ _} {_ _ 1x2 _ 2x1 _} {_ _ _ 2x1 _ 1x2} {_ _ 2x1 _ 1*1 _}] # Dad's Puzzler -F4503bb..21......21..1111....121221......21.. - -# Nine Block (Worse) -F45031212........21..121221......bb..1111.... - +S:[4x5] G:[0,3] M:[F] B:[{2X2 _ 2x1 _} {_ _ 2x1 _} {1x1 1x1 _ _} {1x2 1x2 2x1 _} {_ _ 2x1 _}] +# Nine Blocks +S:[4x5] G:[0,3] M:[F] B:[{1x2 1x2 _ _} {_ _ 2x1 _} {1x2 1x2 2x1 _} {_ _ 2X2 _} {1x1 1x1 _ _}] # Quzzle -F4520bb..21......1212........1221..11..21..11 - +S:[4x5] G:[2,0] M:[F] B:[{2X2 _ 2x1 _} {_ _ 1x2 1x2} {_ _ _ _} {1x2 2x1 _ 1x1} {_ 2x1 _ 1x1}] # Thin Klotski -F451412..ba....22..11......1122..1111....1111 - -# Klotski -F451312bb..12........1221..12..1111..11....11 - +S:[4x5] G:[1,4] M:[F] B:[{1x2 _ 2X1 _} {_ 2x2 _ 1x1} {_ _ _ 1x1} {2x2 _ 1x1 1x1} {_ _ 1x1 1x1}] # Fat Klotski -F4513..bb..1111....121122....11......111121.. - +S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ 1x1} {1x1 _ _ 1x2} {1x1 2x2 _ _} {1x1 _ _ _} {1x1 1x1 2x1 _}] +# Klotski +S:[4x5] G:[1,3] M:[F] B:[{1x2 2X2 _ 1x2} {_ _ _ _} {1x2 2x1 _ 1x2} {_ 1x1 1x1 _} {1x1 _ _ 1x1}] # Century -F451311bb..1112....12..12....11....1121..21.. - +S:[4x5] G:[1,3] M:[F] B:[{1x1 2X2 _ 1x1} {1x2 _ _ 1x2} {_ 1x2 _ _} {1x1 _ _ 1x1} {2x1 _ 2x1 _}] # Super Century -F451312111111..12bb..12........21..11....21.. - +S:[4x5] G:[1,3] M:[F] B:[{1x2 1x1 1x1 1x1} {_ 1x2 2X2 _} {1x2 _ _ _} {_ 2x1 _ 1x1} {_ 2x1 _ _}] # Supercompo -F4513..bb....11....111221..12..21....1121..11 \ No newline at end of file +S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}] \ No newline at end of file diff --git a/flake.nix b/flake.nix index ecd3ad4..e699b30 100644 --- a/flake.nix +++ b/flake.nix @@ -109,6 +109,7 @@ rec { # gnumake cmake ninja + # cling # pkg-config # clang-tools # compdb @@ -118,6 +119,8 @@ rec { hotspot kdePackages.kcachegrind gdbgui + massif-visualizer + heaptrack # renderdoc ]; @@ -129,11 +132,13 @@ rec { raylib raygui thread-pool + boost # Debugging tracy-wayland backward-cpp libbfd + catch2_3 ]; # =========================================================================================== # Define buildable + installable packages @@ -152,8 +157,15 @@ rec { cmakeFlags = [ "-DDISABLE_TRACY=On" "-DDISABLE_BACKWARD=On" + "-DDISABLE_TESTS=On" ]; + hardeningDisable = ["all"]; + + preConfigure = '' + unset NIX_ENFORCE_NO_NATIVE + ''; + installPhase = '' mkdir -p $out/bin cp ./${pname} $out/bin/ @@ -178,6 +190,7 @@ rec { raylib raygui thread-pool + boost ]; cmakeFlags = [ @@ -295,9 +308,12 @@ rec { abbr -a release "${buildRelease} && ./cmake-build-release/masssprings" abbr -a debug-clean "${cmakeDebug} && ${buildDebug} && ./cmake-build-debug/masssprings" abbr -a release-clean "${cmakeRelease} && ${buildRelease} && ./cmake-build-release/masssprings" + abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings" + abbr -a runperf "${buildRelease} && perf record -g ./cmake-build-release/masssprings && hotspot ./perf.data" abbr -a runtracy "tracy -a 127.0.0.1 &; ${buildRelease} && sudo -E ./cmake-build-release/masssprings" abbr -a runvalgrind "${buildDebug} && valgrind --leak-check=full --show-reachable=no --show-leak-kinds=definite,indirect,possible --track-origins=no --suppressions=valgrind.supp --log-file=valgrind.log ./cmake-build-debug/masssprings && cat valgrind.log" + abbr -a runtests "${buildDebug} && ./cmake-build-debug/tests" abbr -a runclion "clion ./CMakeLists.txt 2>/dev/null 1>&2 & disown;" ''; diff --git a/include/input_handler.hpp b/include/input_handler.hpp index 2a8441c..f4f8395 100644 --- a/include/input_handler.hpp +++ b/include/input_handler.hpp @@ -19,7 +19,7 @@ struct show_yes_no_message { std::string title; std::string message; - std::function on_yes; + std::function on_yes; }; struct show_save_preset_window diff --git a/include/puzzle.hpp b/include/puzzle.hpp index c9a0131..b090446 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -7,306 +7,431 @@ #include #include #include +#include #include #include +#include -// A state is represented by a string "MWHXYblocks", where M is "R" -// (restricted) or "F" (free), W is the board width, H is the board height, X -// is the target block x goal, Y is the target block y goal and blocks is an -// enumeration of each of the board's cells, with each cell being a 2-letter -// or 2-digit block representation (a 3x3 board would have a string -// representation with length 5 + 3*3 * 2). The board's cells are enumerated -// from top-left to bottom-right with each block's pivot being its top-left -// corner. +/* + * 8x8 board + * -> 64 (sizes) * 2 (target) * 2 (movable) blocks = 1 Byte + * + * 1. Encode the position inside the board (max 64 cells) + * -> 64 (slots) * 1 Byte (block) = 64 Byte + * 2. Encode the position inside the block (max 64 cells) + * -> 64 (blocks) * 2 Byte (size + pos) = 128 Byte + * 3. Encode the position inside the block (with 15 block limit) + * -> 15 (blocks) * 2 (block) = 30 Byte + * + * Store board size + restricted: +1 Byte + * Store target position: +1 Byte + * + * => Limit to 15 blocks max and use option 3. (4x uint64_t) + * + */ class puzzle { public: - // A block is represented as a 2-digit string "wh", where w is the block width - // and h the block height. - // The target block (to remove from the board) is represented as a 2-letter - // lower-case string "xy", where x is the block width and y the block height and - // width/height are represented by [abcdefghi] (~= [123456789]). - // Immovable blocks are represented as a 2-letter upper-case string "XY", where - // X is the block width and Y the block height and width/height are represented - // by [ABCDEFGHI] (~= [123456789]). + /* + * A block is represented as uint16_t. + * It stores its position, width, height and if it's the target block or immovable. + */ class block { - public: - int x = 0; - int y = 0; - int width = 0; - int height = 0; - bool target = false; - bool immovable = false; - - public: - block(const int _x, const int _y, const int _width, const int _height, - const bool _target = false, const bool _immovable = false) - : x(_x), y(_y), width(_width), height(_height), target(_target), immovable(_immovable) - { - if (_x < 0 || _x + _width > MAX_WIDTH || _y < 0 || _y + _height > MAX_HEIGHT) { - errln("Block must fit in a 9x9 board!"); - exit(1); - } - } - - block(const int _x, const int _y, const std::string& b) : x(_x), y(_y) - { - if (b.length() != 2) { - errln("Block representation must have length 2!"); - exit(1); - } - - if (b == "..") { - this->x = 0; - this->y = 0; - width = 0; - height = 0; - target = false; - return; - } - - target = false; - constexpr std::array target_chars{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'}; - for (const char c : target_chars) { - if (b.contains(c)) { - target = true; - break; - } - } - - immovable = false; - constexpr std::array immovable_chars{'A', 'B', 'C', 'D', 'E', - 'F', 'G', 'H', 'I'}; - for (const char c : immovable_chars) { - if (b.contains(c)) { - immovable = true; - break; - } - } - - if (target) { - width = static_cast(b.at(0)) - static_cast('a') + 1; - height = static_cast(b.at(1)) - static_cast('a') + 1; - } else if (immovable) { - width = static_cast(b.at(0)) - static_cast('A') + 1; - height = static_cast(b.at(1)) - static_cast('A') + 1; - } else { - // println("Parsing block string '{}' at ({}, {})", b, _x, _y); - width = std::stoi(b.substr(0, 1)); - height = std::stoi(b.substr(1, 1)); - } - - if (_x < 0 || _x + width > 9 || _y < 0 || _y + height > 9) { - errln("Block must fit in a 9x9 board!"); - exit(1); - } - } - - block() = delete; - - auto operator==(const block& other) const -> bool - { - return x == other.x && y == other.y && width && other.width && target == other.target && - immovable == other.immovable; - } - - auto operator!=(const block& other) const -> bool - { - return !(*this == other); - } - - public: - [[nodiscard]] auto hash() const -> size_t; - [[nodiscard]] auto valid() const -> bool; - [[nodiscard]] auto string() const -> std::string; - - // Movement - - [[nodiscard]] auto principal_dirs() const -> int; - [[nodiscard]] auto covers(int _x, int _y) const -> bool; - [[nodiscard]] auto collides(const block& b) const -> bool; - }; - -private: - // https://en.cppreference.com/w/cpp/iterator/input_iterator.html - class block_iterator - { - public: - using difference_type = std::ptrdiff_t; - using value_type = block; + friend class puzzle; private: - const puzzle& state; - int current_pos; + static constexpr uint16_t INVALID = 0x8000; + + static constexpr uint8_t IMMOVABLE_S = 0; + static constexpr uint8_t IMMOVABLE_E = 0; + static constexpr uint8_t TARGET_S = 1; + static constexpr uint8_t TARGET_E = 1; + static constexpr uint8_t WIDTH_S = 2; + static constexpr uint8_t WIDTH_E = 4; + static constexpr uint8_t HEIGHT_S = 5; + static constexpr uint8_t HEIGHT_E = 7; + static constexpr uint8_t X_S = 8; + static constexpr uint8_t X_E = 10; + static constexpr uint8_t Y_S = 11; + static constexpr uint8_t Y_E = 13; + + /* + * Memory layout: + * + * 154 321 098 765 432 1 0 + * B0 YYY XXX HHH WWW T I + * ---------- ----------- + * Byte 1 Byte 0 + * + * Store the y-position at the most significant bits to obtain a row-major ordering when comparing reprs. + * The block at (0, 0) will be the smallest, the block at (width, height) the largest, + * the block at (1, 0) will be smaller than the block at (0, 1), all independent of block size. + * Then, the block with size (1, 1) will be smaller than the block with size (2, 2), + * the block with size (1, 0) - horizontal - will be smaller than the block with size (0, 1) - vertical. + * + * To mark if a block is empty, the first B bit is set to 1. This is required, + * since otherwise uint16_t{0} would be a valid block. This also makes empty blocks sorted last. + */ + uint16_t repr; public: - explicit block_iterator(const puzzle& _state) : state(_state), current_pos(0) - {} + // Produces an invalid block, for usage with std::array + block() + : repr(INVALID) {} - block_iterator(const puzzle& _state, const int _current_pos) - : state(_state), current_pos(_current_pos) - {} + explicit block(const uint16_t _repr) + : repr(_repr) {} - auto operator*() const -> block + block(const int x, const int y, const int w, const int h, const bool t = false, const bool i = false) + : repr(create_repr(x, y, w, h, t, i)) { - return {current_pos % state.width, current_pos / state.width, - state.state.substr(current_pos * 2 + PREFIX, 2)}; + if (x < 0 || x + w > MAX_WIDTH || y < 0 || y + h > MAX_HEIGHT) { + throw std::invalid_argument("Block size out of bounds"); + } + if (t && i) { + throw std::invalid_argument("Target block can't be immovable"); + } } - auto operator++() -> block_iterator& + auto operator==(const block other) const -> bool { - do { - current_pos++; - } while (current_pos < state.width * state.height && - state.state.substr(current_pos * 2 + PREFIX, 2) == ".."); - return *this; + return repr == other.repr; } - auto operator==(const block_iterator& other) const -> bool + auto operator!=(const block other) const -> bool { - return state == other.state && current_pos == other.current_pos; + return repr != other.repr; } - auto operator!=(const block_iterator& other) const -> bool + auto operator<(const block other) const -> bool { - return !(*this == other); + return repr < other.repr; } + + auto operator<=(const block other) const -> bool + { + return repr <= other.repr; + } + + auto operator>(const block other) const -> bool + { + return repr > other.repr; + } + + auto operator>=(const block other) const -> bool + { + return repr >= other.repr; + } + + private: + [[nodiscard]] static auto create_repr(uint8_t x, uint8_t y, uint8_t w, uint8_t h, bool t = false, + bool i = false) -> uint16_t; + + // Repr setters + [[nodiscard]] auto set_x(uint8_t x) const -> block; + [[nodiscard]] auto set_y(uint8_t y) const -> block; + [[nodiscard]] auto set_width(uint8_t width) const -> block; + [[nodiscard]] auto set_height(uint8_t height) const -> block; + [[nodiscard]] auto set_target(bool target) const -> block; + [[nodiscard]] auto set_immovable(bool immovable) const -> block; + + public: + [[nodiscard]] auto unpack_repr() const -> std::tuple; + + // Repr getters + [[nodiscard]] auto get_x() const -> uint8_t; + [[nodiscard]] auto get_y() const -> uint8_t; + [[nodiscard]] auto get_width() const -> uint8_t; + [[nodiscard]] auto get_height() const -> uint8_t; + [[nodiscard]] auto get_target() const -> bool; + [[nodiscard]] auto get_immovable() const -> bool; + + // Util + [[nodiscard]] auto valid() const -> bool; + [[nodiscard]] auto principal_dirs() const -> uint8_t; + [[nodiscard]] auto covers(int _x, int _y) const -> bool; + [[nodiscard]] auto collides(block b) const -> bool; }; public: - static constexpr int PREFIX = 5; - static constexpr int MIN_WIDTH = 3; - static constexpr int MIN_HEIGHT = 3; - static constexpr int MAX_WIDTH = 9; - static constexpr int MAX_HEIGHT = 9; + static constexpr uint8_t MAX_BLOCKS = 15; + static constexpr uint8_t MIN_WIDTH = 3; + static constexpr uint8_t MIN_HEIGHT = 3; + static constexpr uint8_t MAX_WIDTH = 8; + static constexpr uint8_t MAX_HEIGHT = 8; - int width = 0; - int height = 0; - int target_x = 9; - int target_y = 9; - bool restricted = false; // Only allow blocks to move in their principal direction - std::string state; +private: + static constexpr uint16_t INVALID = 0x8000; + + static constexpr uint8_t RESTRICTED_S = 0; + static constexpr uint8_t RESTRICTED_E = 0; + static constexpr uint8_t GOAL_X_S = 1; + static constexpr uint8_t GOAL_X_E = 3; + static constexpr uint8_t GOAL_Y_S = 4; + static constexpr uint8_t GOAL_Y_E = 6; + static constexpr uint8_t WIDTH_S = 7; + static constexpr uint8_t WIDTH_E = 9; + static constexpr uint8_t HEIGHT_S = 10; + static constexpr uint8_t HEIGHT_E = 12; + static constexpr uint8_t GOAL_S = 13; + static constexpr uint8_t GOAL_E = 13; + + struct repr_cooked + { + /* + * Memory layout: + * + * 1543 210 98 7 654 321 0 + * B0G HHH WW W YYY XXX R + * ---------- ----------- + * Byte 1 Byte 0 + * + * To mark if a puzzle is empty, the first B bit is set to 1. + * An extra bit is used to mark if the board has a goal, because we can't store MAX_WIDTH=8 in 3 bits. + */ + uint16_t meta; + + // NOTE: For the hashes to work, this array needs to be sorted always. + // NOTE: This array might contain empty blocks at the end. The iterator handles this. + std::array blocks; + + // repr_cooked() = delete; + // repr_cooked(const repr_cooked& copy) = delete; + // repr_cooked(repr_cooked&& move) = delete; + } __attribute__((packed)); + + /** + * With gcc, were allowed to acces the members arbitrarily, even if they're not active (not the ones last written): + * - https://gcc.gnu.org/onlinedocs/gcc-4.7.1/gcc/Structures-unions-enumerations-and-bit_002dfields-implementation.html#Structures-unions-enumerations-and-bit_002dfields-implementation + * - https://gcc.gnu.org/onlinedocs/gcc-4.7.1/gcc/Optimize-Options.html#Type_002dpunning + */ + union repr_u + { + // The representation split into meta information and the blocks array + repr_cooked cooked; + + // For 15 blocks, we have sizeof(meta) + blocks.size() * sizeof(block) = 2 + 15 * 2 = 32 Bytes + std::array raw; + }; + + repr_u repr; public: - puzzle() = delete; + // Produces an invalid puzzle, for usage with containers + puzzle() + : repr(repr_cooked{INVALID, invalid_blocks()}) {} - puzzle(const int w, const int h, const int tx, const int ty, const bool r) - : width(w), height(h), target_x(tx), target_y(ty), restricted(r), - state( - std::format("{}{}{}{}{}{}", r ? "R" : "F", w, h, tx, ty, std::string(w * h * 2, '.'))) + explicit puzzle(const uint16_t meta) + : repr(repr_cooked{meta, invalid_blocks()}) {} + + // NOTE: This constructor does not sort the blocks and is only for state space generation + puzzle(const std::tuple& meta, + const std::array& sorted_blocks) + : repr(repr_cooked{create_meta(meta), sorted_blocks}) {} + + puzzle(const uint64_t byte_0, const uint64_t byte_1, const uint64_t byte_2, const uint64_t byte_3) + : repr(create_repr(byte_0, byte_1, byte_2, byte_3)) {} + + puzzle(const uint16_t meta, const std::array& blocks) + : repr(repr_cooked{meta, blocks}) {} + + puzzle(const uint8_t w, const uint8_t h, const uint8_t tx, const uint8_t ty, const bool r, const bool g) + : repr(create_repr(w, h, tx, ty, r, g, invalid_blocks())) { - if (w < 1 || w > 9 || h < 1 || h > 9) { - errln("State width/height must be in [1, 9]!"); - exit(1); + if (w < MIN_WIDTH || w > MAX_WIDTH || h < MIN_HEIGHT || h > MAX_HEIGHT) { + throw std::invalid_argument("Board size out of bounds"); } - if (tx < 0 || tx >= 9 || ty < 0 || ty >= 9) { - if (tx != 9 && ty != 9) { - errln("State target must be within the board bounds!"); - exit(1); - } + if (tx >= MAX_WIDTH || ty >= MAX_HEIGHT) { + throw std::invalid_argument("Goal out of bounds"); } } - puzzle(const int w, const int h, const bool r) : puzzle(w, h, 9, 9, r) - {} - - explicit puzzle(const std::string& s) - : width(std::stoi(s.substr(1, 1))), height(std::stoi(s.substr(2, 1))), - target_x(std::stoi(s.substr(3, 1))), target_y(std::stoi(s.substr(4, 1))), - restricted(s.substr(0, 1) == "R"), state(s) + puzzle(const uint8_t w, const uint8_t h, const uint8_t tx, const uint8_t ty, const bool r, const bool g, + const std::array& b) + : repr(create_repr(w, h, tx, ty, r, g, b)) { - if (width < 1 || width > 9 || height < 1 || height > 9) { - errln("State width/height must be in [1, 9]!"); - exit(1); + if (w < MIN_WIDTH || w > MAX_WIDTH || h < MIN_HEIGHT || h > MAX_HEIGHT) { + throw std::invalid_argument("Board size out of bounds"); } - if (target_x < 0 || target_x >= 9 || target_y < 0 || target_y >= 9) { - if (target_x != 9 && target_y != 9) { - errln("State target must be within the board bounds!"); - exit(1); - } - } - if (static_cast(s.length()) != width * height * 2 + PREFIX) { - errln("State representation must have length width * " - "height * 2 + {}!", - PREFIX); - exit(1); + if (tx >= MAX_WIDTH || ty >= MAX_HEIGHT) { + throw std::invalid_argument("Goal out of bounds"); } } + explicit puzzle(const std::string& string_repr) + : repr(create_repr(string_repr)) {} + public: auto operator==(const puzzle& other) const -> bool { - return state == other.state; + return repr.raw == other.repr.raw; } auto operator!=(const puzzle& other) const -> bool { - return !(*this == other); + return repr.raw != other.repr.raw; } - [[nodiscard]] auto begin() const -> block_iterator + auto operator<(const puzzle& other) const -> bool { - block_iterator it{*this}; - if (!(*it).valid()) { - ++it; + // Start from MSB and go to LSB. If equal, check the next. + for (int i = 3; i >= 0; --i) { + if (repr.raw[i] < other.repr.raw[i]) { + return true; + } + if (repr.raw[i] > other.repr.raw[i]) { + return false; + } } - return it; + + // All are equal + return false; } - [[nodiscard]] auto end() const -> block_iterator + auto operator<=(const puzzle& other) const -> bool { - return {*this, width * height}; + return *this < other || *this == other; } + auto operator>(const puzzle& other) const -> bool + { + return !(*this <= other); + } + + auto operator>=(const puzzle& other) const -> bool + { + return !(*this < other); + } + + auto repr_view() const + { + return std::span(repr.cooked.blocks.data(), block_count()); + } + + auto block_view() const + { + return std::span(repr.cooked.blocks.data(), block_count()) | std::views::transform( + [](const uint16_t val) + { + return block(val); + }); + } + + template + static auto hash_combine(std::size_t& seed, const T& v, const Rest&... rest) -> void; + private: - [[nodiscard]] auto get_index(int x, int y) const -> int; + [[nodiscard]] static constexpr auto invalid_blocks() -> std::array + { + std::array blocks; + for (size_t i = 0; i < MAX_BLOCKS; ++i) { + blocks[i] = block::INVALID; + } + return blocks; + } + + [[nodiscard]] static auto create_meta(const std::tuple& meta) -> uint16_t; + [[nodiscard]] static auto create_repr(uint8_t w, uint8_t h, uint8_t tx, uint8_t ty, bool r, bool g, + const std::array& b) -> repr_cooked; + + [[nodiscard]] static auto create_repr(uint64_t byte_0, uint64_t byte_1, uint64_t byte_2, + uint64_t byte_3) -> repr_cooked; + + [[nodiscard]] static auto create_repr(const std::string& string_repr) -> repr_cooked; + + // Repr setters + [[nodiscard]] auto set_restricted(bool restricted) const -> puzzle; + [[nodiscard]] auto set_width(uint8_t width) const -> puzzle; + [[nodiscard]] auto set_height(uint8_t height) const -> puzzle; + [[nodiscard]] auto set_goal(bool goal) const -> puzzle; + [[nodiscard]] auto set_goal_x(uint8_t target_x) const -> puzzle; + [[nodiscard]] auto set_goal_y(uint8_t target_y) const -> puzzle; + [[nodiscard]] auto set_blocks(std::array blocks) const -> puzzle; public: + // Repr getters + [[nodiscard]] auto unpack_meta() const -> std::tuple; + [[nodiscard]] auto get_restricted() const -> bool; + [[nodiscard]] auto get_width() const -> uint8_t; + [[nodiscard]] auto get_height() const -> uint8_t; + [[nodiscard]] auto get_goal() const -> bool; + [[nodiscard]] auto get_goal_x() const -> uint8_t; + [[nodiscard]] auto get_goal_y() const -> uint8_t; + + // Util [[nodiscard]] auto hash() const -> size_t; - [[nodiscard]] auto has_win_condition() const -> bool; - [[nodiscard]] auto won() const -> bool; + [[nodiscard]] auto string_repr() const -> std::string; + [[nodiscard]] static auto try_parse_string_repr(const std::string& string_repr) -> std::optional; [[nodiscard]] auto valid() const -> bool; [[nodiscard]] auto try_get_invalid_reason() const -> std::optional; - - // Repr helpers - - [[nodiscard]] auto try_get_block(int x, int y) const -> std::optional; + [[nodiscard]] auto block_count() const -> uint8_t; + [[nodiscard]] auto goal_reached() const -> bool; + [[nodiscard]] auto try_get_block(uint8_t x, uint8_t y) const -> std::optional; [[nodiscard]] auto try_get_target_block() const -> std::optional; - [[nodiscard]] auto covers(int x, int y, int w, int h) const -> bool; - [[nodiscard]] auto covers(int x, int y) const -> bool; - [[nodiscard]] auto covers(const block& b) const -> bool; + [[nodiscard]] auto covers(uint8_t x, uint8_t y, uint8_t _w, uint8_t _h) const -> bool; + [[nodiscard]] auto covers(uint8_t x, uint8_t y) const -> bool; + [[nodiscard]] auto covers(block b) const -> bool; // Editing - - [[nodiscard]] auto try_toggle_restricted() const -> std::optional; - [[nodiscard]] auto try_set_goal(int x, int y) const -> std::optional; - [[nodiscard]] auto try_clear_goal() const -> std::optional; + [[nodiscard]] auto toggle_restricted() const -> puzzle; + [[nodiscard]] auto try_set_goal(uint8_t x, uint8_t y) const -> std::optional; + [[nodiscard]] auto clear_goal() const -> puzzle; [[nodiscard]] auto try_add_column() const -> std::optional; [[nodiscard]] auto try_remove_column() const -> std::optional; [[nodiscard]] auto try_add_row() const -> std::optional; [[nodiscard]] auto try_remove_row() const -> std::optional; - [[nodiscard]] auto try_add_block(const block& b) const -> std::optional; - [[nodiscard]] auto try_remove_block(int x, int y) const -> std::optional; - [[nodiscard]] auto try_toggle_target(int x, int y) const -> std::optional; - [[nodiscard]] auto try_toggle_wall(int x, int y) const -> std::optional; + [[nodiscard]] auto try_add_block(block b) const -> std::optional; + [[nodiscard]] auto try_remove_block(uint8_t x, uint8_t y) const -> std::optional; + [[nodiscard]] auto try_toggle_target(uint8_t x, uint8_t y) const -> std::optional; + [[nodiscard]] auto try_toggle_wall(uint8_t x, uint8_t y) const -> std::optional; // Playing - - [[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const - -> std::optional; + [[nodiscard]] auto try_move_block_at(uint8_t x, uint8_t y, direction dir) const -> std::optional; // Statespace + [[nodiscard]] auto try_move_block_at_fast(uint64_t bitmap, uint8_t block_idx, + direction dir) const -> std::optional; + static auto sorted_replace(std::array blocks, uint8_t idx, + uint16_t new_val) -> std::array; + auto blocks_bitmap() const -> uint64_t; + static inline auto bitmap_set_bit(uint64_t bitmap, uint8_t x, uint8_t y) -> uint64_t; + static inline auto bitmap_get_bit(uint64_t bitmap, uint8_t x, uint8_t y) -> bool; + static auto bitmap_clear_block(uint64_t bitmap, block b) -> uint64_t; + static auto bitmap_check_collision(uint64_t bitmap, block b) -> bool; + static auto bitmap_check_collision(uint64_t bitmap, block b, direction dir) -> bool; + + template + auto for_each_adjacent(F&& callback) const -> void + { + const uint64_t bitmap = blocks_bitmap(); + const bool r = get_restricted(); + for (uint8_t idx = 0; idx < MAX_BLOCKS; idx++) { + const block b = block(repr.cooked.blocks[idx]); + if (!b.valid()) { + break; + } + if (b.get_immovable()) { + continue; + } + const int dirs = r ? b.principal_dirs() : nor | eas | sou | wes; + for (const direction d : {nor, eas, sou, wes}) { + if (dirs & d) { + if (auto moved = try_move_block_at_fast(bitmap, idx, d)) { + callback(*moved); + } + } + } + } + } - [[nodiscard]] auto find_adjacent_puzzles() const -> std::vector; [[nodiscard]] auto explore_state_space() const -> std::pair, std::vector>>; }; -// Provide hash functions so we can use State and as hash-set -// keys for masses and springs. -template <> -struct std::hash +// Hash functions for sets and maps + +struct puzzle_hasher { auto operator()(const puzzle& s) const noexcept -> size_t { @@ -314,30 +439,44 @@ struct std::hash } }; -template <> -struct std::hash> +struct link_hasher { auto operator()(const std::pair& s) const noexcept -> size_t { - const size_t h1 = std::hash{}(s.first); - const size_t h2 = std::hash{}(s.second); - - return h1 + h2 + h1 * h2; - // return (h1 ^ h2) + 0x9e3779b9 + (std::min(h1, h2) << 6) + (std::max(h1, h2) >> 2); + size_t h = 0; + if (s.first < s.second) { + puzzle::hash_combine(h, s.first, s.second); + } else { + puzzle::hash_combine(h, s.second, s.first); + } + return h; } }; -template <> -struct std::equal_to> +struct link_equal_to { - auto operator()(const std::pair& a, - const std::pair& b) const noexcept -> bool + auto operator()(const std::pair& a, const std::pair& b) const noexcept -> bool { - return (a.first == b.first && a.second == b.second) || - (a.first == b.second && a.second == b.first); + return (a.first == b.first && a.second == b.second) || (a.first == b.second && a.second == b.first); } }; -using win_condition = std::function; +template +auto puzzle::hash_combine(std::size_t& seed, const T& v, const Rest&... rest) -> void +{ + auto h = [](const HashedType& val) -> std::size_t + { + if constexpr (std::is_same_v, puzzle>) { + return puzzle_hasher{}(val); + } else if constexpr (std::is_same_v, std::pair>) { + return link_hasher{}(val); + } else { + return std::hash>{}(val); + } + }; + + seed ^= h(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + (hash_combine(seed, rest), ...); +} #endif \ No newline at end of file diff --git a/include/state_manager.hpp b/include/state_manager.hpp index a0b02ff..3b3bf50 100644 --- a/include/state_manager.hpp +++ b/include/state_manager.hpp @@ -6,8 +6,8 @@ #include "puzzle.hpp" #include -#include -#include +#include +#include class state_manager { @@ -16,26 +16,23 @@ private: std::string preset_file; size_t current_preset = 0; - std::vector preset_states = {puzzle(4, 5, 9, 9, false)}; - std::vector preset_comments = {"Empty"}; + std::vector preset_states = {puzzle(4, 5, 0, 0, true, false)}; + std::vector preset_comments = {"# Empty"}; // State storage (store states twice for bidirectional lookup). // Everything else should only store indices to state_pool. std::vector state_pool; // Indices are equal to mass_springs mass indices - std::unordered_map state_indices; // Maps states to indices - std::vector> - links; // Indices are equal to mass_springs springs indices + boost::unordered_flat_map state_indices; // Maps states to indices + std::vector> links; // Indices are equal to mass_springs springs indices - graph_distances node_target_distances; // Buffered and reused if the graph doesn't change - std::unordered_set winning_indices; // Indices of all states where the board is solved - std::vector - winning_path; // Ordered list of node indices leading to the nearest solved state - std::unordered_set - path_indices; // For faster lookup if a vertex is part of the path in renderer + graph_distances node_target_distances; // Buffered and reused if the graph doesn't change + boost::unordered_flat_set winning_indices; // Indices of all states where the board is solved + std::vector winning_path; // Ordered list of node indices leading to the nearest solved state + boost::unordered_flat_set path_indices; // For faster lookup if a vertex is part of the path in renderer - std::vector move_history; // Moves between the starting state and the current state - std::unordered_map visit_counts; // How often each state was visited + std::vector move_history; // Moves between the starting state and the current state + boost::unordered_flat_map visit_counts; // How often each state was visited size_t starting_state_index = 0; size_t current_state_index = 0; @@ -45,7 +42,8 @@ private: bool edited = false; public: - state_manager(threaded_physics& _physics, const std::string& _preset_file) : physics(_physics) + state_manager(threaded_physics& _physics, const std::string& _preset_file) + : physics(_physics) { parse_preset_file(_preset_file); load_preset(0); @@ -138,10 +136,10 @@ public: [[nodiscard]] auto get_link_count() const -> size_t; [[nodiscard]] auto get_path_length() const -> size_t; [[nodiscard]] auto get_links() const -> const std::vector>&; - [[nodiscard]] auto get_winning_indices() const -> const std::unordered_set&; - [[nodiscard]] auto get_visit_counts() const -> const std::unordered_map&; + [[nodiscard]] auto get_winning_indices() const -> const boost::unordered_flat_set&; + [[nodiscard]] auto get_visit_counts() const -> const boost::unordered_flat_map&; [[nodiscard]] auto get_winning_path() const -> const std::vector&; - [[nodiscard]] auto get_path_indices() const -> const std::unordered_set&; + [[nodiscard]] auto get_path_indices() const -> const boost::unordered_flat_set&; [[nodiscard]] auto get_current_visits() const -> int; [[nodiscard]] auto get_current_preset() const -> size_t; [[nodiscard]] auto get_preset_count() const -> size_t; diff --git a/include/user_interface.hpp b/include/user_interface.hpp index a922e89..50c7bac 100644 --- a/include/user_interface.hpp +++ b/include/user_interface.hpp @@ -89,7 +89,7 @@ private: grid board_grid = grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, - state.get_current_state().width, state.get_current_state().height, BOARD_PADDING); + state.get_current_state().get_width(), state.get_current_state().get_height(), BOARD_PADDING); grid graph_overlay_grid = grid(GetScreenWidth() / 2, MENU_HEIGHT, 200, 100, 1, 4, MENU_PAD); @@ -100,7 +100,7 @@ private: std::string message_title; std::string message_message; - std::function yes_no_handler; + std::function yes_no_handler; bool ok_message = false; bool yes_no_message = false; bool save_window = false; diff --git a/include/util.hpp b/include/util.hpp index df62772..4d2ce84 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -3,20 +3,66 @@ #include #include -#include -inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream& +// Bit shifting + masking + +template + requires std::unsigned_integral +auto create_mask(const uint8_t first, const uint8_t last) -> T { - os << "(" << v.x << ", " << v.y << ")"; - return os; + // If the mask width is equal the type width return all 1s instead of shifting + // as shifting by type-width is undefined behavior. + if (static_cast(last - first + 1) >= sizeof(T) * 8) { + return ~T{0}; + } + + // Example: first=4, last=7, 7-4+1=4 + // 1 << 4 = 0b00010000 + // 32 - 1 = 0b00001111 + // 31 << 4 = 0b11110000 + // Subtracting 1 generates a consecutive mask. + return ((T{1} << (last - first + 1)) - 1) << first; } -inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream& +template + requires std::unsigned_integral +auto clear_bits(T& bits, const uint8_t first, const uint8_t last) -> void { - os << "(" << v.x << ", " << v.y << ", " << v.z << ")"; - return os; + const T mask = create_mask(first, last); + + bits = bits & ~mask; } +template + requires std::unsigned_integral && std::unsigned_integral +auto set_bits(T& bits, const uint8_t first, const uint8_t last, const U value) -> void +{ + const T mask = create_mask(first, last); + + // Example: first=4, last=6, value=0b1110, bits = 0b 01111110 + // mask = 0b 01110000 + // bits & ~mask = 0b 00001110 + // value << 4 = 0b 11100000 + // (value << 4) & mask = 0b 01100000 + // (bits & ~mask) | (value << 4) & mask = 0b 01101110 + // Insert position: ^^^ + // First clear the bits, then | with the value positioned at the insertion point. + // The value may be larger than [first, last], extra bits are ignored. + bits = (bits & ~mask) | ((static_cast(value) << first) & mask); +} + +template + requires std::unsigned_integral +auto get_bits(const T bits, const uint8_t first, const uint8_t last) -> T +{ + const T mask = create_mask(first, last); + + // We can >> without sign extension because T is unsigned_integral + return (bits & mask) >> first; +} + +// std::variant visitor + // https://en.cppreference.com/w/cpp/utility/variant/visit template struct overloads : Ts... @@ -24,6 +70,8 @@ struct overloads : Ts... using Ts::operator()...; }; +// Enums + enum direction { nor = 1 << 0, @@ -67,6 +115,20 @@ enum bg bg_white = 47 }; +// Output + +inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream& +{ + os << "(" << v.x << ", " << v.y << ")"; + return os; +} + +inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream& +{ + os << "(" << v.x << ", " << v.y << ", " << v.z << ")"; + return os; +} + inline auto ansi_bold_fg(const fg color) -> std::string { return std::format("\033[1;{}m", static_cast(color)); @@ -81,22 +143,22 @@ inline auto ansi_reset() -> std::string template auto infoln(std::format_string fmt, Args&&... args) -> void { - std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset()) - << std::format(fmt, std::forward(args)...) << std::endl; + std::cout << std::format("[{}INFO{}]: ", ansi_bold_fg(fg_blue), ansi_reset()) << std::format( + fmt, std::forward(args)...) << std::endl; } template auto warnln(std::format_string fmt, Args&&... args) -> void { - std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset()) - << std::format(fmt, std::forward(args)...) << std::endl; + std::cout << std::format("[{}WARNING{}]: ", ansi_bold_fg(fg_yellow), ansi_reset()) << std::format( + fmt, std::forward(args)...) << std::endl; } template auto errln(std::format_string fmt, Args&&... args) -> void { - std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset()) - << std::format(fmt, std::forward(args)...) << std::endl; + std::cout << std::format("[{}ERROR{}]: ", ansi_bold_fg(fg_red), ansi_reset()) << std::format( + fmt, std::forward(args)...) << std::endl; } #endif \ No newline at end of file diff --git a/src/input_handler.cpp b/src/input_handler.cpp index 7e8b084..8183beb 100644 --- a/src/input_handler.cpp +++ b/src/input_handler.cpp @@ -162,11 +162,12 @@ auto input_handler::select_block() -> void auto input_handler::start_add_block() -> void { const puzzle& current = state.get_current_state(); - if (!editing || current.try_get_block(hov_x, hov_y) || has_block_add_xy) { + if (!editing || current.try_get_block(hov_x, hov_y) || has_block_add_xy || current.block_count() >= + puzzle::MAX_BLOCKS) { return; } - if (hov_x >= 0 && hov_x < current.width && hov_y >= 0 && hov_y < current.height) { + if (hov_x >= 0 && hov_x < current.get_width() && hov_y >= 0 && hov_y < current.get_height()) { block_add_x = hov_x; block_add_y = hov_y; has_block_add_xy = true; @@ -187,7 +188,8 @@ auto input_handler::clear_add_block() -> void auto input_handler::add_block() -> void { const puzzle& current = state.get_current_state(); - if (!editing || current.try_get_block(hov_x, hov_y) || !has_block_add_xy) { + if (!editing || current.try_get_block(hov_x, hov_y) || !has_block_add_xy || current.block_count() >= + puzzle::MAX_BLOCKS) { return; } @@ -276,7 +278,7 @@ auto input_handler::toggle_camera_projection() const -> void auto input_handler::move_block_nor() -> void { const puzzle& current = state.get_current_state(); - const std::optional& next = current.try_move_block_at(sel_x, sel_y, nor); + const std::optional& next = current.try_move_block_at_fast(sel_x, sel_y, nor); if (!next) { return; } @@ -288,7 +290,7 @@ auto input_handler::move_block_nor() -> void auto input_handler::move_block_wes() -> void { const puzzle& current = state.get_current_state(); - const std::optional& next = current.try_move_block_at(sel_x, sel_y, wes); + const std::optional& next = current.try_move_block_at_fast(sel_x, sel_y, wes); if (!next) { return; } @@ -300,7 +302,7 @@ auto input_handler::move_block_wes() -> void auto input_handler::move_block_sou() -> void { const puzzle& current = state.get_current_state(); - const std::optional& next = current.try_move_block_at(sel_x, sel_y, sou); + const std::optional& next = current.try_move_block_at_fast(sel_x, sel_y, sou); if (!next) { return; } @@ -312,7 +314,7 @@ auto input_handler::move_block_sou() -> void auto input_handler::move_block_eas() -> void { const puzzle& current = state.get_current_state(); - const std::optional& next = current.try_move_block_at(sel_x, sel_y, eas); + const std::optional& next = current.try_move_block_at_fast(sel_x, sel_y, eas); if (!next) { return; } @@ -323,7 +325,7 @@ auto input_handler::move_block_eas() -> void auto input_handler::print_state() const -> void { - infoln("State: \"{}\"", state.get_current_state().state); + infoln("State: \"{}\"", state.get_current_state().string_repr()); } auto input_handler::load_previous_preset() -> void @@ -332,7 +334,7 @@ auto input_handler::load_previous_preset() -> void return; } - const auto handler = [&]() + const auto handler = [&] { block_add_x = -1; block_add_y = -1; @@ -353,7 +355,7 @@ auto input_handler::load_next_preset() -> void return; } - const auto handler = [&]() + const auto handler = [&] { block_add_x = -1; block_add_y = -1; @@ -370,7 +372,7 @@ auto input_handler::load_next_preset() -> void auto input_handler::goto_starting_state() -> void { - const auto handler = [&]() + const auto handler = [&] { state.goto_starting_state(); sel_x = 0; @@ -387,7 +389,7 @@ auto input_handler::populate_graph() const -> void auto input_handler::clear_graph() -> void { - const auto handler = [&]() + const auto handler = [&] { state.clear_graph_and_add_current(); }; @@ -433,7 +435,7 @@ auto input_handler::goto_previous_state() const -> void auto input_handler::toggle_restricted_movement() const -> void { const puzzle& current = state.get_current_state(); - const std::optional& next = current.try_toggle_restricted(); + const std::optional& next = current.toggle_restricted(); if (!editing || !next) { return; @@ -471,7 +473,7 @@ auto input_handler::remove_board_column() const -> void const puzzle& current = state.get_current_state(); const std::optional& next = current.try_remove_column(); - if (!editing || current.width <= puzzle::MIN_WIDTH || !next) { + if (!editing || current.get_width() <= puzzle::MIN_WIDTH || !next) { return; } @@ -483,7 +485,7 @@ auto input_handler::add_board_column() const -> void const puzzle& current = state.get_current_state(); const std::optional& next = current.try_add_column(); - if (!editing || current.width >= puzzle::MAX_WIDTH || !next) { + if (!editing || current.get_width() >= puzzle::MAX_WIDTH || !next) { return; } @@ -495,7 +497,7 @@ auto input_handler::remove_board_row() const -> void const puzzle& current = state.get_current_state(); const std::optional& next = current.try_remove_row(); - if (!editing || current.height <= puzzle::MIN_HEIGHT || !next) { + if (!editing || current.get_height() <= puzzle::MIN_HEIGHT || !next) { return; } @@ -507,7 +509,7 @@ auto input_handler::add_board_row() const -> void const puzzle& current = state.get_current_state(); const std::optional& next = current.try_add_row(); - if (!editing || current.height >= puzzle::MAX_HEIGHT || !next) { + if (!editing || current.get_height() >= puzzle::MAX_HEIGHT || !next) { return; } @@ -528,7 +530,7 @@ auto input_handler::toggle_editing() -> void auto input_handler::clear_goal() const -> void { const puzzle& current = state.get_current_state(); - const std::optional& next = current.try_clear_goal(); + const std::optional& next = current.clear_goal(); if (!editing || !next) { return; diff --git a/src/main.cpp b/src/main.cpp index 6abb953..fe83b1c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,21 +11,14 @@ #include "user_interface.hpp" #ifdef TRACY - #include +#include #endif -// TODO: Add some popups (my split between input.cpp/gui.cpp makes this ugly) -// - Clear graph: Notify that this will clear the visited states and move -// history -// - Reset state: Notify that this will reset the move count +// TODO: Manual movement is now completely broken -// TODO: Reduce memory usage -// - The memory model of the puzzle board is terrible (bitboards?) +// TODO: Add state space generation time to debug overlay // TODO: Improve solver -// - Move discovery is terrible -// - Instead of trying each direction for each block, determine the -// possible moves more efficiently (requires a different memory model) // - Implement state discovery/enumeration // - Find all possible initial board states (single one for each // possible statespace). Currently wer're just finding all states @@ -37,7 +30,18 @@ // TODO: Click states in the graph to display them in the board -// NOTE: Tracy uses a huge amount of memory. For longer testing disable Tracy. +// For profiling explore_state_space +auto main2(int argc, char* argv[]) -> int +{ + const puzzle p = puzzle( + "S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]"); + + for (int i = 0; i < 50; ++i) { + auto space = p.explore_state_space(); + } + + return 0; +} auto main(int argc, char* argv[]) -> int { @@ -48,17 +52,17 @@ auto main(int argc, char* argv[]) -> int preset_file = argv[1]; } -#ifdef BACKWARD + #ifdef BACKWARD infoln("Backward stack-traces enabled."); -#else + #else infoln("Backward stack-traces disabled."); -#endif + #endif -#ifdef TRACY + #ifdef TRACY infoln("Tracy adapter enabled."); -#else + #else infoln("Tracy adapter disabled."); -#endif + #endif // RayLib window setup SetTraceLogLevel(LOG_ERROR); @@ -89,9 +93,9 @@ auto main(int argc, char* argv[]) -> int // Game loop while (!WindowShouldClose()) { -#ifdef TRACY + #ifdef TRACY FrameMarkStart("MainThread"); -#endif + #endif // Time tracking std::chrono::time_point now = std::chrono::high_resolution_clock::now(); @@ -102,16 +106,16 @@ auto main(int argc, char* argv[]) -> int // Input update input.handle_input(); -// Read positions from physics thread -#ifdef TRACY + // Read positions from physics thread + #ifdef TRACY FrameMarkStart("MainThreadConsumeLock"); -#endif + #endif { -#ifdef TRACY + #ifdef TRACY std::unique_lock lock(physics.state.data_mtx); -#else + #else std::unique_lock lock(physics.state.data_mtx); -#endif + #endif ups = physics.state.ups; mass_center = physics.state.mass_center; @@ -130,16 +134,15 @@ auto main(int argc, char* argv[]) -> int physics.state.data_consumed_cnd.notify_all(); } } -#ifdef TRACY + #ifdef TRACY FrameMarkEnd("MainThreadConsumeLock"); -#endif + #endif // Update the camera after the physics, so target lock is smooth size_t current_index = state.get_current_index(); if (masses.size() > current_index) { const mass_spring_system::mass& current_mass = mass_spring_system::mass(masses.at(current_index)); - camera.update(current_mass.position, mass_center, input.camera_lock, - input.camera_mass_center_lock); + camera.update(current_mass.position, mass_center, input.camera_lock, input.camera_mass_center_lock); } // Rendering @@ -153,13 +156,12 @@ auto main(int argc, char* argv[]) -> int } ++loop_iterations; -#ifdef TRACY - FrameMark; - FrameMarkEnd("MainThread"); -#endif + #ifdef TRACY + FrameMark; FrameMarkEnd("MainThread"); + #endif } CloseWindow(); return 0; -} \ No newline at end of file +} diff --git a/src/octree.cpp b/src/octree.cpp index e953bff..3eaa893 100644 --- a/src/octree.cpp +++ b/src/octree.cpp @@ -5,7 +5,7 @@ #include #ifdef TRACY - #include +#include #endif auto octree::node::child_count() const -> int @@ -32,9 +32,8 @@ auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) - auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int { const node& n = nodes[node_idx]; - auto [cx, cy, cz] = - Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, - (n.box_min.z + n.box_max.z) / 2.0f); + auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, + (n.box_min.z + n.box_max.z) / 2.0f); // The octant is encoded as a 3-bit integer "zyx". The node area is split // along all 3 axes, if a position is right of an axis, this bit is set to 1. @@ -54,13 +53,11 @@ auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int return octant; } -auto octree::get_child_bounds(const int node_idx, const int octant) const - -> std::pair +auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair { const node& n = nodes[node_idx]; - auto [cx, cy, cz] = - Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, - (n.box_min.z + n.box_max.z) / 2.0f); + auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, + (n.box_min.z + n.box_max.z) / 2.0f); Vector3 min = Vector3Zero(); Vector3 max = Vector3Zero(); @@ -84,13 +81,11 @@ auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, c // infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y, // pos.z, node_idx, depth); if (depth > MAX_DEPTH) { - errln("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})", node_idx, - nodes[node_idx].box_min.x, nodes[node_idx].box_min.y, nodes[node_idx].box_min.z, - nodes[node_idx].box_max.x, nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, - pos.x, pos.y, pos.z); - - // This runs from inside the physics thread so it won't exit cleanly - exit(1); + throw std::runtime_error(std::format("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})", + node_idx, nodes[node_idx].box_min.x, nodes[node_idx].box_min.y, + nodes[node_idx].box_min.z, nodes[node_idx].box_max.x, + nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, pos.x, pos.y, + pos.z)); } // NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during @@ -152,12 +147,9 @@ auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, c // Update the center of mass const float new_mass = nodes[node_idx].mass_total + mass; - nodes[node_idx].mass_center.x = - (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass; - nodes[node_idx].mass_center.y = - (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass; - nodes[node_idx].mass_center.z = - (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass; + nodes[node_idx].mass_center.x = (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass; + nodes[node_idx].mass_center.y = (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass; + nodes[node_idx].mass_center.z = (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass; nodes[node_idx].mass_total = new_mass; } @@ -198,4 +190,4 @@ auto octree::calculate_force(const int node_idx, const Vector3& pos) const -> Ve } return force; -} +} \ No newline at end of file diff --git a/src/puzzle.cpp b/src/puzzle.cpp index 69c41e9..1407e94 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -1,46 +1,142 @@ #include "puzzle.hpp" -#include -#include +#include +#include -#ifdef TRACY - #include -#endif - -auto puzzle::block::hash() const -> size_t +auto puzzle::block::create_repr(const uint8_t x, const uint8_t y, const uint8_t w, const uint8_t h, const bool t, + const bool i) -> uint16_t { - const std::string s = std::format("{},{},{},{}", x, y, width, height); - return std::hash{}(s); + return block().set_x(x).set_y(y).set_width(w).set_height(h).set_target(t).set_immovable(i).repr & ~INVALID; +} + +auto puzzle::block::set_x(const uint8_t x) const -> block +{ + if (x > 7) { + throw std::invalid_argument("Block x-position out of bounds"); + } + + block b = *this; + set_bits(b.repr, X_S, X_E, x); + return b; +} + +auto puzzle::block::set_y(const uint8_t y) const -> block +{ + if (y > 7) { + throw std::invalid_argument("Block y-position out of bounds"); + } + + block b = *this; + set_bits(b.repr, Y_S, Y_E, y); + return b; +} + +auto puzzle::block::set_width(const uint8_t width) const -> block +{ + if (width - 1 > 7) { + throw std::invalid_argument("Block width out of bounds"); + } + + block b = *this; + set_bits(b.repr, WIDTH_S, WIDTH_E, width - 1u); + return b; +} + +auto puzzle::block::set_height(const uint8_t height) const -> block +{ + if (height - 1 > 7) { + throw std::invalid_argument("Block height out of bounds"); + } + + block b = *this; + set_bits(b.repr, HEIGHT_S, HEIGHT_E, height - 1u); + return b; +} + +auto puzzle::block::set_target(const bool target) const -> block +{ + block b = *this; + set_bits(b.repr, TARGET_S, TARGET_E, target); + return b; +} + +auto puzzle::block::set_immovable(const bool immovable) const -> block +{ + block b = *this; + set_bits(b.repr, IMMOVABLE_S, IMMOVABLE_E, immovable); + return b; +} + +auto puzzle::block::unpack_repr() const -> std::tuple +{ + const uint8_t x = get_x(); + const uint8_t y = get_y(); + const uint8_t w = get_width(); + const uint8_t h = get_height(); + const bool t = get_target(); + const bool i = get_immovable(); + + return {x, y, w, h, t, i}; +} + +auto puzzle::block::get_x() const -> uint8_t +{ + return get_bits(repr, X_S, X_E); +} + +auto puzzle::block::get_y() const -> uint8_t +{ + return get_bits(repr, Y_S, Y_E); +} + +auto puzzle::block::get_width() const -> uint8_t +{ + return get_bits(repr, WIDTH_S, WIDTH_E) + 1u; +} + +auto puzzle::block::get_height() const -> uint8_t +{ + return get_bits(repr, HEIGHT_S, HEIGHT_E) + 1u; +} + +auto puzzle::block::get_target() const -> bool +{ + return get_bits(repr, TARGET_S, TARGET_E); +} + +auto puzzle::block::get_immovable() const -> bool +{ + return get_bits(repr, IMMOVABLE_S, IMMOVABLE_E); } auto puzzle::block::valid() const -> bool { - return width > 0 && height > 0 && x >= 0 && x + width <= 9 && y >= 0 && y + height <= 9; + const auto [x, y, w, h, t, i] = unpack_repr(); + + if (t && i) { + return false; + } + + // This means the first bit is set, marking the block as empty + if (repr & INVALID) { + return false; + } + + return w > 0 && h > 0 && x + w <= MAX_WIDTH && y + h <= MAX_HEIGHT; } -auto puzzle::block::string() const -> std::string +auto puzzle::block::principal_dirs() const -> uint8_t { - if (target) { - return std::format("{}{}", static_cast(width + static_cast('a') - 1), - static_cast(height + static_cast('a') - 1)); - } - if (immovable) { - return std::format("{}{}", static_cast(width + static_cast('A') - 1), - static_cast(height + static_cast('A') - 1)); - } - return std::format("{}{}", width, height); -} + const auto [x, y, w, h, t, i] = unpack_repr(); -auto puzzle::block::principal_dirs() const -> int -{ - if (immovable) { + if (i) { return 0; } - if (width > height) { + if (w > h) { return eas | wes; } - if (height > width) { + if (h > w) { return nor | sou; } return nor | eas | sou | wes; @@ -48,131 +144,456 @@ auto puzzle::block::principal_dirs() const -> int auto puzzle::block::covers(const int _x, const int _y) const -> bool { - return _x >= x && _x < x + width && _y >= y && _y < y + height; + const auto [x, y, w, h, t, i] = unpack_repr(); + + return _x >= x && _x < x + w && _y >= y && _y < y + h; } -auto puzzle::block::collides(const block& b) const -> bool +auto puzzle::block::collides(const block b) const -> bool { - return x < b.x + b.width && x + width > b.x && y < b.y + b.height && y + height > b.y; + const auto [x, y, w, h, t, i] = unpack_repr(); + const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); + + return x < bx + bw && x + w > bx && y < by + bh && y + h > by; } -auto puzzle::get_index(const int x, const int y) const -> int +auto puzzle::create_meta(const std::tuple& meta) -> uint16_t { - if (x < 0 || x >= width || y < 0 || y >= height) { - errln("Trying to calculating index of invalid board coordinates ({}, {})", x, y); - exit(1); + const auto [w, h, gx, gy, r, g] = meta; + + uint16_t m = 0; + set_bits(m, WIDTH_S, WIDTH_E, w - 1u); + set_bits(m, HEIGHT_S, HEIGHT_E, h - 1u); + set_bits(m, GOAL_X_S, GOAL_X_E, gx); + set_bits(m, GOAL_Y_S, GOAL_Y_E, gy); + set_bits(m, RESTRICTED_S, RESTRICTED_E, r); + set_bits(m, GOAL_S, GOAL_E, g); + return m; +} + +auto puzzle::create_repr(const uint8_t w, const uint8_t h, const uint8_t tx, const uint8_t ty, const bool r, + const bool g, const std::array& b) -> repr_cooked +{ + repr_cooked repr = puzzle().set_width(w).set_height(h).set_goal_x(tx).set_goal_y(ty).set_restricted(r).set_goal(g). + set_blocks(b).repr.cooked; + repr.meta &= ~INVALID; + return repr; +} + +auto puzzle::create_repr(const uint64_t byte_0, const uint64_t byte_1, const uint64_t byte_2, + const uint64_t byte_3) -> repr_cooked +{ + repr_u repr{}; + repr.raw = std::array{byte_0, byte_1, byte_2, byte_3}; + + return repr.cooked; +} + +auto puzzle::create_repr(const std::string& string_repr) -> repr_cooked +{ + const std::optional repr = try_parse_string_repr(string_repr); + if (!repr) { + throw std::invalid_argument("Failed to parse string repr"); } - return PREFIX + (y * width + x) * 2; + + return *repr; +} + +auto puzzle::set_restricted(const bool restricted) const -> puzzle +{ + uint16_t meta = repr.cooked.meta; + set_bits(meta, RESTRICTED_S, RESTRICTED_E, restricted); + return puzzle(meta, repr.cooked.blocks); +} + +auto puzzle::set_width(const uint8_t width) const -> puzzle +{ + if (width - 1 > MAX_WIDTH) { + throw "Board width out of bounds"; + } + + uint16_t meta = repr.cooked.meta; + set_bits(meta, WIDTH_S, WIDTH_E, width - 1u); + return puzzle(meta, repr.cooked.blocks); +} + +auto puzzle::set_height(const uint8_t height) const -> puzzle +{ + if (height - 1 > MAX_HEIGHT) { + throw "Board height out of bounds"; + } + + uint16_t meta = repr.cooked.meta; + set_bits(meta, HEIGHT_S, HEIGHT_E, height - 1u); + return puzzle(meta, repr.cooked.blocks); +} + +auto puzzle::set_goal(const bool goal) const -> puzzle +{ + uint16_t meta = repr.cooked.meta; + set_bits(meta, GOAL_S, GOAL_E, goal); + return puzzle(meta, repr.cooked.blocks); +} + +auto puzzle::set_goal_x(const uint8_t target_x) const -> puzzle +{ + if (target_x >= MAX_WIDTH) { + throw "Board target x out of bounds"; + } + + uint16_t meta = repr.cooked.meta; + set_bits(meta, GOAL_X_S, GOAL_X_E, target_x); + return puzzle(meta, repr.cooked.blocks); +} + +auto puzzle::set_goal_y(const uint8_t target_y) const -> puzzle +{ + if (target_y >= MAX_HEIGHT) { + throw "Board target y out of bounds"; + } + + uint16_t meta = repr.cooked.meta; + set_bits(meta, GOAL_Y_S, GOAL_Y_E, target_y); + return puzzle(meta, repr.cooked.blocks); +} + +auto puzzle::set_blocks(std::array blocks) const -> puzzle +{ + puzzle p = *this; + std::ranges::sort(blocks); + p.repr.cooked.blocks = blocks; + return p; +} + +auto puzzle::unpack_meta() const -> std::tuple +{ + const uint8_t w = get_width(); + const uint8_t h = get_height(); + const uint8_t tx = get_goal_x(); + const uint8_t ty = get_goal_y(); + const bool r = get_restricted(); + const bool g = get_goal(); + + return {w, h, tx, ty, r, g}; +} + +auto puzzle::get_restricted() const -> bool +{ + return get_bits(repr.cooked.meta, RESTRICTED_S, RESTRICTED_E); +} + +auto puzzle::get_width() const -> uint8_t +{ + return get_bits(repr.cooked.meta, WIDTH_S, WIDTH_E) + 1u; +} + +auto puzzle::get_height() const -> uint8_t +{ + return get_bits(repr.cooked.meta, HEIGHT_S, HEIGHT_E) + 1u; +} + +auto puzzle::get_goal() const -> bool +{ + return get_bits(repr.cooked.meta, GOAL_S, GOAL_E); +} + +auto puzzle::get_goal_x() const -> uint8_t +{ + return get_bits(repr.cooked.meta, GOAL_X_S, GOAL_X_E); +} + +auto puzzle::get_goal_y() const -> uint8_t +{ + return get_bits(repr.cooked.meta, GOAL_Y_S, GOAL_Y_E); } auto puzzle::hash() const -> size_t { - return std::hash{}(state); + size_t h = 0; + for (size_t i = 0; i < 4; ++i) { + hash_combine(h, repr.raw[i]); + } + return h; } -auto puzzle::has_win_condition() const -> bool +auto puzzle::string_repr() const -> std::string { - return target_x != MAX_WIDTH && target_y != MAX_HEIGHT; + // S:[3x3] G:[1,1] M:[R] B:[{1x1 _ _} {2X1 _ _} {_ _ _}] + const auto [w, h, gx, gy, r, g] = unpack_meta(); + + std::vector linear_blocks(w * h); + for (const block b : block_view()) { + linear_blocks[b.get_y() * w + b.get_x()] = b; + } + + std::string size = std::format("S:[{}x{}] ", w, h); + std::string goal = g ? std::format("G:[{},{}] ", gx, gy) : ""; + std::string restricted = std::format("M:[{}] ", r ? "R" : "F"); + + std::string blocks = "B:["; + for (int y = 0; y < h; ++y) { + blocks += '{'; + for (int x = 0; x < w; ++x) { + const block b = block(linear_blocks[y * w + x]); + if (!b.valid()) { + blocks += "_ "; + } else if (b.get_target()) { + blocks += std::format("{}X{} ", b.get_width(), b.get_height()); + } else if (b.get_immovable()) { + blocks += std::format("{}*{} ", b.get_width(), b.get_height()); + } else { + blocks += std::format("{}x{} ", b.get_width(), b.get_height()); + } + } + blocks.pop_back(); // Remove last extra space before } + blocks += "} "; + } + blocks.pop_back(); // Remove last extra space before ] + blocks += ']'; + + return std::format("{}{}{}{}", size, goal, restricted, blocks); } -auto puzzle::won() const -> bool +auto puzzle::try_parse_string_repr(const std::string& string_repr) -> std::optional { - const std::optional& b = try_get_target_block(); - return has_win_condition() && b && b->x == target_x && b->y == target_y; + bool parsed_size = false; + std::pair size{0, 0}; + + bool parsed_goal = false; + std::pair goal{0, 0}; + + bool parsed_restricted = false; + bool restricted = true; + + bool parsed_blocks = false; + std::vector bs; + std::array blocks = invalid_blocks(); + + const auto digit = [&](const char c) + { + return std::string("0123456789").contains(c); + }; + + // S:[3x3] + const auto parse_size = [&](size_t& pos) + { + uint8_t w = std::stoi(string_repr.substr(pos + 3, 1)); + uint8_t h = std::stoi(string_repr.substr(pos + 5, 1)); + size = std::make_pair(w, h); + + parsed_size = true; + pos += 6; + }; + + // G:[1,1] (optional) + const auto parse_goal = [&](size_t& pos) + { + uint8_t gx = std::stoi(string_repr.substr(pos + 3, 1)); + uint8_t gy = std::stoi(string_repr.substr(pos + 5, 1)); + goal = std::make_pair(gx, gy); + + parsed_goal = true; + pos += 6; + }; + + // M:[R] + const auto parse_restricted = [&](size_t& pos) + { + if (string_repr[pos + 3] == 'R') { + restricted = true; + parsed_restricted = true; + } + if (string_repr[pos + 3] == 'F') { + restricted = false; + parsed_restricted = true; + } + + pos += 4; + }; + + // 1x1 or 1X1 or 1*1 or _ + const auto parse_block = [&](size_t& pos, const uint8_t x, const uint8_t y) + { + if (string_repr[pos] == '_') { + return block(); + } + + const uint8_t w = std::stoi(string_repr.substr(pos, 1)); + const uint8_t h = std::stoi(string_repr.substr(pos + 2, 1)); + const bool t = string_repr[pos + 1] == 'X'; + const bool i = string_repr[pos + 1] == '*'; + + pos += 2; + return block(x, y, w, h, t, i); + }; + + // {1x1 _ _} + const auto parse_row = [&](size_t& pos, const uint8_t y) + { + std::vector row; + + uint8_t x = 0; + ++pos; // Skip { + while (string_repr[pos] != '}') { + if (digit(string_repr[pos]) || string_repr[pos] == '_') { + const block b = parse_block(pos, x, y); + if (b.valid()) { + row.emplace_back(b.repr); + } + ++x; + } + ++pos; + } + + return row; + }; + + // B:[{1x1 _ _} {2X1 _ _} {_ _ _}] + const auto parse_blocks = [&](size_t& pos) + { + std::vector rows; + + uint8_t y = 0; + ++pos; // Skip [ + while (string_repr[pos] != ']') { + if (string_repr[pos] == '{') { + std::vector row = parse_row(pos, y); + rows.insert(rows.end(), row.begin(), row.end()); + ++y; + } + ++pos; + } + + parsed_blocks = true; + return rows; + }; + + /* + * User-readable string representation (3x3 example): + * + * S:[3x3] G:[1,1] M:[R] B:[{1x1 _ _} {2X1 _ _} {_ _ _}] + */ + for (size_t pos = 0; pos < string_repr.size(); ++pos) { + switch (string_repr[pos]) { + case 'S': + if (parsed_size) { + warnln("Parsed duplicate attribute"); + return std::nullopt; + } + parse_size(pos); + break; + case 'G': + if (parsed_goal) { + warnln("Parsed duplicate attribute"); + return std::nullopt; + } + parse_goal(pos); + break; + case 'M': + if (parsed_restricted) { + warnln("Parsed duplicate attribute"); + return std::nullopt; + } + parse_restricted(pos); + break; + case 'B': + if (parsed_blocks) { + warnln("Parsed duplicate attribute"); + return std::nullopt; + } + bs = parse_blocks(pos); + + if (bs.size() > MAX_BLOCKS) { + warnln("Parsed too many blocks"); + return std::nullopt; + } + + std::copy_n(bs.begin(), bs.size(), blocks.begin()); + break; + default: + break; + } + } + + if (!parsed_size || !parsed_restricted || !parsed_blocks) { + warnln("Failed to parse required attribute"); + return std::nullopt; + } + + return create_repr(size.first, size.second, goal.first, goal.second, restricted, parsed_goal, blocks); } auto puzzle::valid() const -> bool { - return width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT; + if (repr.cooked.meta & INVALID) { + return false; + } + + const auto [w, h, gx, gy, r, g] = unpack_meta(); + + return w >= MIN_WIDTH && w <= MAX_WIDTH && h >= MIN_HEIGHT && h <= MAX_HEIGHT; } auto puzzle::try_get_invalid_reason() const -> std::optional { + if (repr.cooked.meta & INVALID) { + return "Flagged as Invalid"; + } + + const auto [w, h, gx, gy, r, g] = unpack_meta(); + + infoln("Validating puzzle {}", string_repr()); + const std::optional& b = try_get_target_block(); - if (has_win_condition() && !b) { + if (get_goal() && !b) { return "Goal Without Target"; } - if (!has_win_condition() && b) { + if (!get_goal() && b) { return "Target Without Goal"; } - if (has_win_condition() && b && restricted) { + if (get_goal() && b && r) { const int dirs = b->principal_dirs(); - if ((dirs & nor && b->x != target_x) || (dirs & eas && b->y != target_y)) { + if ((dirs & nor && b->get_x() != gx) || (dirs & eas && b->get_y() != gy)) { return "Goal Unreachable"; } } - if (target_x > 0 && target_x + b->width < width && target_y > 0 && target_y + b->height < height) { + if (get_goal() && b && gx > 0 && gx + b->get_width() < w && gy > 0 && gy + b->get_height() < h) { return "Goal Inside"; } - infoln("Validating puzzle {}", state); - - if (static_cast(state.length()) != width * height * 2 + PREFIX) { - infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(), - width, height); - return "Invalid Repr Length"; - } - - // Check prefix - if (!std::string("FR").contains(state[0])) { - infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]); - return "Invalid Restricted Repr"; - } - if (restricted && state[0] != 'R') { - infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0], - restricted); - return "Restricted != Restricted Repr"; - } - if (!std::string("0123456789").contains(state[1]) || - !std::string("0123456789").contains(state[2])) { - infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match [3-9]", state[1], state[2]); - return "Invalid Dims Repr"; - } - if (std::stoi(state.substr(1, 1)) != width || std::stoi(state.substr(2, 1)) != height) { - infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match {}x{} board", state[1], - state[2], width, height); - return "Dims != Dims Repr"; - } - if (!std::string("0123456789").contains(state[3]) || - !std::string("0123456789").contains(state[4])) { - infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match [1-9]", state[3], state[4]); - return "Invalid Goal Repr"; - } - if (std::stoi(state.substr(3, 1)) != target_x || std::stoi(state.substr(4, 1)) != target_y) { - infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match target ({}, {})", state[3], - state[4], target_x, target_y); - return "Goal != Goal Repr"; - } - - // Check blocks - const std::string allowed_chars = ".123456789abcdefghiABCDEFGHI"; - for (const char c : state.substr(PREFIX, state.length() - PREFIX)) { - if (!allowed_chars.contains(c)) { - infoln("Puzzle invalid: Block {} has invalid character", c); - return "Invalid Block Repr"; - } - } - - const bool success = - width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT; - - if (!success) { - infoln("Puzzle invalid: Board size {}x{} not in range [3-9]", width, height); + if (!valid()) { return "Invalid Dims"; - } else { - return std::nullopt; } + + return std::nullopt; } -auto puzzle::try_get_block(const int x, const int y) const -> std::optional +auto puzzle::block_count() const -> uint8_t +{ + uint8_t count = 0; + for (const uint16_t b : repr.cooked.blocks) { + if (block(b).valid()) { + ++count; + } + } + return count; +} + +auto puzzle::goal_reached() const -> bool +{ + const std::optional b = try_get_target_block(); + return get_goal() && b && b->get_x() == get_goal_x() && b->get_y() == get_goal_y(); +} + +auto puzzle::try_get_block(const uint8_t x, const uint8_t y) const -> std::optional { if (!covers(x, y)) { return std::nullopt; } - for (const block& b : *this) { + for (const block b : block_view()) { if (b.covers(x, y)) { return b; } @@ -183,8 +604,8 @@ auto puzzle::try_get_block(const int x, const int y) const -> std::optional std::optional { - for (const block b : *this) { - if (b.target) { + for (const block b : block_view()) { + if (b.get_target()) { return b; } } @@ -192,244 +613,271 @@ auto puzzle::try_get_target_block() const -> std::optional return std::nullopt; } -auto puzzle::covers(const int x, const int y, const int w, const int h) const -> bool +auto puzzle::covers(const uint8_t x, const uint8_t y, const uint8_t _w, const uint8_t _h) const -> bool { - return x >= 0 && x + w <= width && y >= 0 && y + h <= height; + return x + _w <= get_width() && y + _h <= get_height(); } -auto puzzle::covers(const int x, const int y) const -> bool +auto puzzle::covers(const uint8_t x, const uint8_t y) const -> bool { return covers(x, y, 1, 1); } -auto puzzle::covers(const block& b) const -> bool +auto puzzle::covers(const block b) const -> bool { - return covers(b.x, b.y, b.width, b.height); + return covers(b.get_x(), b.get_y(), b.get_width(), b.get_height()); } -auto puzzle::try_toggle_restricted() const -> std::optional +auto puzzle::toggle_restricted() const -> puzzle { - puzzle p = *this; - p.restricted = !restricted; - p.state.replace(0, 1, p.restricted ? "R" : "F"); - return p; + return set_restricted(!get_restricted()); } -auto puzzle::try_set_goal(const int x, const int y) const -> std::optional +auto puzzle::try_set_goal(const uint8_t x, const uint8_t y) const -> std::optional { const std::optional& b = try_get_target_block(); - if (!b || !covers(x, y, b->width, b->height)) { + if (!b || !covers(x, y, b->get_width(), b->get_height())) { return std::nullopt; } - puzzle p = *this; - if (target_x == x && target_y == y) { - p.target_x = MAX_WIDTH; - p.target_y = MAX_HEIGHT; - } else { - p.target_x = x; - p.target_y = y; + if (get_goal_x() == x && get_goal_y() == y) { + return clear_goal(); } - p.state.replace(3, 1, std::format("{}", p.target_x)); - p.state.replace(4, 1, std::format("{}", p.target_y)); - - return p; + return set_goal_x(x).set_goal_y(y).set_goal(true); } -auto puzzle::try_clear_goal() const -> std::optional +auto puzzle::clear_goal() const -> puzzle { - puzzle p = *this; - p.target_x = MAX_WIDTH; - p.target_y = MAX_HEIGHT; - p.state.replace(3, 2, std::format("{}{}", MAX_WIDTH, MAX_HEIGHT)); - return p; + return set_goal_x(0).set_goal_y(0).set_goal(false); } auto puzzle::try_add_column() const -> std::optional { - if (width >= MAX_WIDTH) { + const uint8_t w = get_width(); + if (w >= MAX_WIDTH) { return std::nullopt; } - puzzle p = {width + 1, height, restricted}; - - // Non-fitting blocks won't be added - for (const block& b : *this) { - if (const std::optional& _p = p.try_add_block(b)) { - p = *_p; - } - } - - return p; + return set_width(w + 1); } auto puzzle::try_remove_column() const -> std::optional { - if (width <= MIN_WIDTH) { + const auto [w, h, gx, gy, r, g] = unpack_meta(); + if (w <= MIN_WIDTH) { return std::nullopt; } - puzzle p = {width - 1, height, restricted}; + puzzle p{static_cast(w - 1), h, 0, 0, r, g}; - // Non-fitting blocks won't be added - for (const block& b : *this) { + // Re-add all the blocks, blocks no longer fitting won't be added + for (const block b : block_view()) { if (const std::optional& _p = p.try_add_block(b)) { p = *_p; } } + const std::optional b = p.try_get_target_block(); + if (p.get_goal() && b && !p.covers(gx, gy, b->get_width(), b->get_height())) { + // Target no longer inside board + return p.clear_goal(); + } + if (p.get_goal() && !b) { + // Target block removed during resize + return p.clear_goal(); + } + return p; } auto puzzle::try_add_row() const -> std::optional { - if (height >= 9) { + const uint8_t h = get_height(); + if (h >= MAX_HEIGHT) { return std::nullopt; } - puzzle p = puzzle(width, height + 1, restricted); - - for (const block& b : *this) { - if (const std::optional& _p = p.try_add_block(b)) { - p = *_p; - } - } - - return p; + return set_height(h + 1); } auto puzzle::try_remove_row() const -> std::optional { - if (height == 0) { + const auto [w, h, gx, gy, r, g] = unpack_meta(); + if (h <= MIN_HEIGHT) { return std::nullopt; } - puzzle p = puzzle(width, height - 1, restricted); + puzzle p{w, static_cast(h - 1), gx, gy, r, g}; - for (const block& b : *this) { + // Re-add all the blocks, blocks no longer fitting won't be added + for (const block b : block_view()) { if (const std::optional& _p = p.try_add_block(b)) { p = *_p; } } + const std::optional b = p.try_get_target_block(); + if (p.get_goal() && b && !p.covers(gx, gy, b->get_width(), b->get_height())) { + // Target no longer inside board + return p.clear_goal(); + } + if (p.get_goal() && !b) { + // Target block removed during resize + return p.clear_goal(); + } + return p; } -auto puzzle::try_add_block(const block& b) const -> std::optional +auto puzzle::try_add_block(const block b) const -> std::optional { + const uint8_t count = block_count(); + if (count == MAX_BLOCKS) { + return std::nullopt; + } + if (!covers(b)) { return std::nullopt; } - for (block _b : *this) { + for (const block _b : block_view()) { if (_b.collides(b)) { return std::nullopt; } } - puzzle p = *this; - const int index = get_index(b.x, b.y); - p.state.replace(index, 2, b.string()); + const auto [w, h, gx, gy, r, g] = unpack_meta(); + std::array blocks = repr.cooked.blocks; - return p; + // This requires all empty blocks being at the end of the array (otherwise we might overwrite). + // This is the case because empty blocks' most significant bit is 1 and the array is sorted. + blocks[count] = b.repr; + + return puzzle(w, h, gx, gy, r, g, blocks); } -auto puzzle::try_remove_block(const int x, const int y) const -> std::optional +auto puzzle::try_remove_block(const uint8_t x, const uint8_t y) const -> std::optional { const std::optional& b = try_get_block(x, y); if (!b) { return std::nullopt; } - puzzle p = *this; - const int index = get_index(b->x, b->y); - p.state.replace(index, 2, ".."); + const auto [w, h, gx, gy, r, g] = unpack_meta(); + std::array blocks = repr.cooked.blocks; + for (uint16_t& _b : blocks) { + if (_b == b->repr) { + _b = block().repr; + } + } - return p; + return puzzle(w, h, gx, gy, r, g, blocks); } -auto puzzle::try_toggle_target(const int x, const int y) const -> std::optional +auto puzzle::try_toggle_target(const uint8_t x, const uint8_t y) const -> std::optional { - std::optional b = try_get_block(x, y); - if (!b || b->immovable) { + const std::optional b = try_get_block(x, y); + if (!b || b->get_immovable()) { return std::nullopt; } - // Remove the current target if it exists - puzzle p = *this; - if (const std::optional& _b = try_get_target_block()) { - const int index = get_index(_b->x, _b->y); - p.state.replace(index, 2, block(_b->x, _b->y, _b->width, _b->height, false).string()); + const auto [w, h, gx, gy, r, g] = unpack_meta(); + std::array blocks = repr.cooked.blocks; + + for (uint16_t& _b : blocks) { + if (!block(_b).valid()) { + // Empty blocks are at the end + break; + } + + if (_b != b->repr) { + // Remove the old target(s) + _b = block(_b).set_target(false).repr; + } else { + // Add the new target + _b = block(_b).set_target(!b->get_target()).repr; + } } - // Add the new target - b->target = !b->target; - const int index = get_index(b->x, b->y); - p.state.replace(index, 2, b->string()); + const block _b = block(gx, gy, b->get_width(), b->get_height()); + if (covers(_b)) { + // Old goal still valid + return puzzle(w, h, gx, gy, r, g, blocks); + } - return p; + return puzzle(w, h, 0, 0, r, g, blocks); } -auto puzzle::try_toggle_wall(const int x, const int y) const -> std::optional +auto puzzle::try_toggle_wall(const uint8_t x, const uint8_t y) const -> std::optional { - std::optional b = try_get_block(x, y); - if (!b || b->target) { + const std::optional b = try_get_block(x, y); + if (!b || b->get_target()) { return std::nullopt; } - // Add the new target - puzzle p = *this; - b->immovable = !b->immovable; - const int index = get_index(b->x, b->y); - p.state.replace(index, 2, b->string()); + const auto [w, h, gx, gy, r, g] = unpack_meta(); + std::array blocks = repr.cooked.blocks; - return p; + for (uint16_t& _b : blocks) { + if (!block(_b).valid()) { + // Empty blocks are at the end + break; + } + + if (_b == b->repr) { + // Toggle wall + _b = block(_b).set_immovable(!b->get_immovable()).repr; + } + } + + return puzzle(w, h, gx, gy, r, g, blocks); } -auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const - -> std::optional +auto puzzle::try_move_block_at(const uint8_t x, const uint8_t y, const direction dir) const -> std::optional { - const std::optional& b = try_get_block(x, y); - if (!b || b->immovable) { + const std::optional b = try_get_block(x, y); + const auto [bx, by, bw, bh, bt, bi] = b->unpack_repr(); + if (!b || bi) { return std::nullopt; } - const int dirs = restricted ? b->principal_dirs() : nor | eas | sou | wes; + const auto [w, h, gx, gy, r, g] = unpack_meta(); + const int dirs = r ? b->principal_dirs() : nor | eas | sou | wes; // Get target block - int _target_x = b->x; - int _target_y = b->y; + int _target_x = bx; + int _target_y = by; switch (dir) { case nor: if (!(dirs & nor) || _target_y < 1) { return std::nullopt; } - _target_y--; + --_target_y; break; case eas: - if (!(dirs & eas) || _target_x + b->width >= width) { + if (!(dirs & eas) || _target_x + bw >= w) { return std::nullopt; } - _target_x++; + ++_target_x; break; case sou: - if (!(dirs & sou) || _target_y + b->height >= height) { + if (!(dirs & sou) || _target_y + bh >= h) { return std::nullopt; } - _target_y++; + ++_target_y; break; case wes: if (!(dirs & wes) || _target_x < 1) { return std::nullopt; } - _target_x--; + --_target_x; break; } - const block moved_b = block(_target_x, _target_y, b->width, b->height, b->target); + const block moved_b = block(_target_x, _target_y, bw, bh, bt); // Check collisions - for (const block& _b : *this) { + for (const block _b : block_view()) { if (_b != b && _b.collides(moved_b)) { return std::nullopt; } @@ -448,86 +896,220 @@ auto puzzle::try_move_block_at(const int x, const int y, const direction dir) co return p; } -auto puzzle::find_adjacent_puzzles() const -> std::vector +auto puzzle::try_move_block_at_fast(const uint64_t bitmap, const uint8_t block_idx, + const direction dir) const -> std::optional { - std::vector puzzles; + const block b = block(repr.cooked.blocks[block_idx]); + const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); + if (bi) { + return std::nullopt; + } - for (const block& b : *this) { - if (b.immovable) { - continue; + const auto [w, h, gx, gy, r, g] = unpack_meta(); + const int dirs = r ? b.principal_dirs() : nor | eas | sou | wes; + + // Get target block + int _target_x = bx; + int _target_y = by; + switch (dir) { + case nor: + if (!(dirs & nor) || _target_y < 1) { + return std::nullopt; + } + --_target_y; + break; + case eas: + if (!(dirs & eas) || _target_x + bw >= w) { + return std::nullopt; + } + ++_target_x; + break; + case sou: + if (!(dirs & sou) || _target_y + bh >= h) { + return std::nullopt; + } + ++_target_y; + break; + case wes: + if (!(dirs & wes) || _target_x < 1) { + return std::nullopt; + } + --_target_x; + break; + } + + // Check collisions + const uint64_t bm = bitmap_clear_block(bitmap, b); + if (bitmap_check_collision(bm, b, dir)) { + return std::nullopt; + } + + // Replace block + const std::array blocks = sorted_replace(repr.cooked.blocks, block_idx, + block::create_repr( + _target_x, _target_y, bw, bh, bt)); + + // This constructor doesn't sort + return puzzle(std::make_tuple(w, h, gx, gy, r, g), blocks); +} + +auto puzzle::sorted_replace(std::array blocks, const uint8_t idx, + const uint16_t new_val) -> std::array +{ + // Remove old entry + for (uint8_t i = idx; i < MAX_BLOCKS - 1; ++i) { + blocks[i] = blocks[i + 1]; + } + blocks[MAX_BLOCKS - 1] = block::INVALID; + + // Find insertion point for new_val + uint8_t insert_at = 0; + while (insert_at < MAX_BLOCKS && blocks[insert_at] < new_val) { + ++insert_at; + } + + // Shift right and insert + for (uint8_t i = MAX_BLOCKS - 1; i > insert_at; --i) { + blocks[i] = blocks[i - 1]; + } + blocks[insert_at] = new_val; + + return blocks; +} + +auto puzzle::blocks_bitmap() const -> uint64_t +{ + uint64_t bitmap = 0; + for (uint8_t i = 0; i < MAX_BLOCKS; ++i) { + block b(repr.cooked.blocks[i]); + if (!b.valid()) { + break; } - const int dirs = restricted ? b.principal_dirs() : nor | eas | sou | wes; + auto [x, y, w, h, t, im] = b.unpack_repr(); - if (dirs & nor) { - if (const std::optional& north = try_move_block_at(b.x, b.y, nor)) { - puzzles.push_back(*north); + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + bitmap |= 1ULL << ((y + dy) * 8 + (x + dx)); } } + } + return bitmap; +} - if (dirs & eas) { - if (const std::optional& east = try_move_block_at(b.x, b.y, eas)) { - puzzles.push_back(*east); - } +inline auto puzzle::bitmap_set_bit(const uint64_t bitmap, const uint8_t x, const uint8_t y) -> uint64_t +{ + return bitmap & ~(1ULL << (y * 8 + x)); +} + +inline auto puzzle::bitmap_get_bit(const uint64_t bitmap, const uint8_t x, const uint8_t y) -> bool +{ + return bitmap & (1ULL << (y * 8 + x)); +} + +auto puzzle::bitmap_clear_block(uint64_t bitmap, const block b) -> uint64_t +{ + const auto [x, y, w, h, t, i] = b.unpack_repr(); + + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + bitmap = bitmap_set_bit(bitmap, x + dx, y + dy); } + } - if (dirs & sou) { - if (const std::optional& south = try_move_block_at(b.x, b.y, sou)) { - puzzles.push_back(*south); - } - } + return bitmap; +} - if (dirs & wes) { - if (const std::optional& west = try_move_block_at(b.x, b.y, wes)) { - puzzles.push_back(*west); +auto puzzle::bitmap_check_collision(const uint64_t bitmap, const block b) -> bool +{ + const auto [x, y, w, h, t, i] = b.unpack_repr(); + + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + if (bitmap_get_bit(bitmap, x + dx, y + dy)) { + return true; // collision } } } - // for (const puzzle& p : puzzles) { - // println("Adjacent puzzle: {}", p.state); - // } - - return puzzles; + return false; } -auto puzzle::explore_state_space() const - -> std::pair, std::vector>> +auto puzzle::bitmap_check_collision(const uint64_t bitmap, const block b, const direction dir) -> bool { -#ifdef TRACY - ZoneScoped; -#endif + const auto [x, y, w, h, t, i] = b.unpack_repr(); - // infoln("Exploring state space, this might take a while..."); + switch (dir) { + case nor: // Check the row above: (x...x+w-1, y-1) + for (int dx = 0; dx < w; ++dx) { + if (bitmap_get_bit(bitmap, x + dx, y - 1)) { + return true; + } + } + break; + case sou: // Check the row below: (x...x+w-1, y+h) + for (int dx = 0; dx < w; ++dx) { + if (bitmap_get_bit(bitmap, x + dx, y + h)) { + return true; + } + } + break; + case wes: // Check the column left: (x-1, y...y+h-1) + for (int dy = 0; dy < h; ++dy) { + if (bitmap_get_bit(bitmap, x - 1, y + dy)) { + return true; + } + } + break; + case eas: // Check the column right: (x+w, y...y+h-1) + for (int dy = 0; dy < h; ++dy) { + if (bitmap_get_bit(bitmap, x + w, y + dy)) { + return true; + } + } + break; + } + return false; +} + +auto puzzle::explore_state_space() const -> std::pair, std::vector>> +{ + const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); std::vector state_pool; - std::unordered_map state_indices; // Helper to construct the links vector - std::vector> links; + boost::unordered_flat_map state_indices; + std::vector> links; // Buffer for all states we want to call GetNextStates() on - std::unordered_set remaining_states; - remaining_states.insert(*this); + std::vector queue; // indices into state_pool - do { - const puzzle current = *remaining_states.begin(); - remaining_states.erase(current); + // Start with the current state + state_indices.emplace(*this, 0); + state_pool.push_back(*this); + queue.push_back(0); - if (!state_indices.contains(current)) { - state_indices.emplace(current, state_pool.size()); - state_pool.push_back(current); - } + size_t head = 0; + while (head < queue.size()) { + const size_t current_idx = queue[head++]; - for (const puzzle& s : current.find_adjacent_puzzles()) { - if (!state_indices.contains(s)) { - remaining_states.insert(s); - state_indices.emplace(s, state_pool.size()); - state_pool.push_back(s); + // Make a copy because references might be invalidated when inserting into the vector + const puzzle current = state_pool[current_idx]; + + current.for_each_adjacent([&](const puzzle& p) + { + auto [it, inserted] = state_indices.emplace(p, state_pool.size()); + if (inserted) { + state_pool.push_back(p); + queue.push_back(it->second); } - links.emplace_back(state_indices.at(current), state_indices.at(s)); - } - } while (!remaining_states.empty()); + links.emplace_back(current_idx, it->second); + }); + } + const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); + + infoln("Explored puzzle. Took {} ms.", std::chrono::duration_cast(end - start).count()); infoln("State space has size {} with {} transitions.", state_pool.size(), links.size()); - return std::make_pair(state_pool, links); + return {std::move(state_pool), std::move(links)}; } \ No newline at end of file diff --git a/src/state_manager.cpp b/src/state_manager.cpp index a387537..8a3d6e9 100644 --- a/src/state_manager.cpp +++ b/src/state_manager.cpp @@ -91,7 +91,7 @@ auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool std::vector comment_lines; std::vector preset_lines; while (std::getline(file, line)) { - if (line.starts_with("F") || line.starts_with("R")) { + if (line.starts_with("S")) { preset_lines.push_back(line); } else if (line.starts_with("#")) { comment_lines.push_back(line); @@ -105,10 +105,11 @@ auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool preset_states.clear(); for (const auto& preset : preset_lines) { + // Each char is a bit const puzzle& p = puzzle(preset); if (const std::optional& reason = p.try_get_invalid_reason()) { - preset_states = {puzzle(4, 5, 9, 9, false)}; + preset_states = {puzzle(4, 5, 0, 0, true, false)}; infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason); return false; } @@ -135,7 +136,7 @@ auto state_manager::append_preset_file(const std::string& preset_name) -> bool return false; } - file << "\n# " << preset_name << "\n" << get_current_state().state << std::flush; + file << "\n# " << preset_name << "\n" << get_current_state().string_repr() << std::flush; infoln("Refreshing presets..."); if (parse_preset_file(preset_file)) { @@ -189,7 +190,7 @@ auto state_manager::update_current_state(const puzzle& p) -> void move_history.emplace_back(previous_state_index); } - if (p.won()) { + if (p.goal_reached()) { winning_indices.insert(current_state_index); } @@ -297,6 +298,7 @@ auto state_manager::populate_graph() -> void const puzzle s = get_starting_state(); const puzzle p = get_current_state(); + // Clear the graph first so we don't add duplicates somehow synced_clear_statespace(); @@ -340,7 +342,7 @@ auto state_manager::populate_winning_indices() -> void { winning_indices.clear(); for (const auto& [state, index] : state_indices) { - if (state.won()) { + if (state.goal_reached()) { winning_indices.insert(index); } } @@ -429,12 +431,12 @@ auto state_manager::get_links() const -> const std::vector const std::unordered_set& +auto state_manager::get_winning_indices() const -> const boost::unordered_flat_set& { return winning_indices; } -auto state_manager::get_visit_counts() const -> const std::unordered_map& +auto state_manager::get_visit_counts() const -> const boost::unordered_flat_map& { return visit_counts; } @@ -444,7 +446,7 @@ auto state_manager::get_winning_path() const -> const std::vector& return winning_path; } -auto state_manager::get_path_indices() const -> const std::unordered_set& +auto state_manager::get_path_indices() const -> const boost::unordered_flat_set& { return path_indices; } diff --git a/src/user_interface.cpp b/src/user_interface.cpp index 6cbb6dc..82344ce 100644 --- a/src/user_interface.cpp +++ b/src/user_interface.cpp @@ -8,12 +8,11 @@ #include #ifdef TRACY - #include +#include #endif -auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, - const int _height, const int _columns, const int _rows) - -> void +auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height, + const int _columns, const int _rows) -> void { x = _x; y = _y; @@ -23,8 +22,7 @@ auto user_interface::grid::update_bounds(const int _x, const int _y, const int _ rows = _rows; } -auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, - const int _height) -> void +auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height) -> void { x = _x; y = _y; @@ -48,19 +46,17 @@ auto user_interface::grid::bounds() const -> Rectangle return bounds; } -auto user_interface::grid::bounds(const int _x, const int _y, const int _width, - const int _height) const -> Rectangle +auto user_interface::grid::bounds(const int _x, const int _y, const int _width, const int _height) const -> Rectangle { if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { - errln("Grid bounds are outside range."); - exit(1); + throw std::invalid_argument("Grid bounds out of range"); } const int cell_width = (width - padding) / columns; const int cell_height = (height - padding) / rows; - return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, - _width * cell_width - padding, _height * cell_height - padding); + return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, _width * cell_width - padding, + _height * cell_height - padding); } auto user_interface::grid::square_bounds() const -> Rectangle @@ -80,8 +76,7 @@ auto user_interface::grid::square_bounds(const int _x, const int _y, const int _ // filled if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { - errln("Grid bounds are outside range."); - exit(1); + throw std::invalid_argument("Grid bounds out of range"); } const int available_width = width - padding * (columns + 1); @@ -93,10 +88,8 @@ auto user_interface::grid::square_bounds(const int _x, const int _y, const int _ const int x_offset = (width - grid_width) / 2; const int y_offset = (height - grid_height) / 2; - return Rectangle(x_offset + _x * (cell_size + padding) + padding, - y_offset + _y * (cell_size + padding) + padding, - _width * cell_size + padding * (_width - 1), - _height * cell_size + padding * (_height - 1)); + return Rectangle(x_offset + _x * (cell_size + padding) + padding, y_offset + _y * (cell_size + padding) + padding, + _width * cell_size + padding * (_width - 1), _height * cell_size + padding * (_height - 1)); } auto user_interface::init() -> void @@ -155,19 +148,19 @@ auto user_interface::get_default_style() -> default_style { // Could've iterated over the values, but then it wouldn't be as nice to // access... - return {{GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL), - GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED), - GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED), - GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED), - GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED), - GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED)}, - GuiGetStyle(DEFAULT, BACKGROUND_COLOR), - GuiGetStyle(DEFAULT, LINE_COLOR), - GuiGetStyle(DEFAULT, TEXT_SIZE), - GuiGetStyle(DEFAULT, TEXT_SPACING), - GuiGetStyle(DEFAULT, TEXT_LINE_SPACING), - GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), - GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)}; + return { + { + GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL), + GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED), + GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED), + GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED), + GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED), + GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED) + }, + GuiGetStyle(DEFAULT, BACKGROUND_COLOR), GuiGetStyle(DEFAULT, LINE_COLOR), GuiGetStyle(DEFAULT, TEXT_SIZE), + GuiGetStyle(DEFAULT, TEXT_SPACING), GuiGetStyle(DEFAULT, TEXT_LINE_SPACING), + GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), GuiGetStyle(DEFAULT, TEXT_WRAP_MODE) + }; } auto user_interface::set_default_style(const default_style& style) -> void @@ -201,15 +194,17 @@ auto user_interface::set_default_style(const default_style& style) -> void auto user_interface::get_component_style(const int component) -> component_style { return { - {GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL), - GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED), - GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED), - GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED), - GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED), - GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)}, - GuiGetStyle(component, BORDER_WIDTH), - GuiGetStyle(component, TEXT_PADDING), - GuiGetStyle(component, TEXT_ALIGNMENT)}; + { + GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL), + GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED), + GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED), + GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED), + GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED), + GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED) + }, + GuiGetStyle(component, BORDER_WIDTH), GuiGetStyle(component, TEXT_PADDING), + GuiGetStyle(component, TEXT_ALIGNMENT) + }; } auto user_interface::set_component_style(const int component, const component_style& style) -> void @@ -238,13 +233,11 @@ auto user_interface::set_component_style(const int component, const component_st auto user_interface::popup_bounds() -> Rectangle { return Rectangle(static_cast(GetScreenWidth()) / 2.0f - POPUP_WIDTH / 2.0f, - static_cast(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f, - POPUP_WIDTH, POPUP_HEIGHT); + static_cast(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f, POPUP_WIDTH, POPUP_HEIGHT); } -auto user_interface::draw_button(const Rectangle bounds, const std::string& label, - const Color color, const bool enabled, const int font_size) const - -> int +auto user_interface::draw_button(const Rectangle bounds, const std::string& label, const Color color, + const bool enabled, const int font_size) const -> int { // Save original styling const default_style original_default = get_default_style(); @@ -275,16 +268,16 @@ auto user_interface::draw_button(const Rectangle bounds, const std::string& labe } auto user_interface::draw_menu_button(const int x, const int y, const int width, const int height, - const std::string& label, const Color color, - const bool enabled, const int font_size) const -> int + const std::string& label, const Color color, const bool enabled, + const int font_size) const -> int { const Rectangle bounds = menu_grid.bounds(x, y, width, height); return draw_button(bounds, label, color, enabled, font_size); } auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::string& off_label, - const std::string& on_label, int* active, Color color, - bool enabled, int font_size) const -> int + const std::string& on_label, int* active, Color color, bool enabled, + int font_size) const -> int { // Save original styling const default_style original_default = get_default_style(); @@ -319,18 +312,16 @@ auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::strin return pressed; } -auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width, - const int height, const std::string& off_label, - const std::string& on_label, int* active, - const Color color, const bool enabled, - const int font_size) const -> int +auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width, const int height, + const std::string& off_label, const std::string& on_label, int* active, + const Color color, const bool enabled, const int font_size) const -> int { const Rectangle bounds = menu_grid.bounds(x, y, width, height); return draw_toggle_slider(bounds, off_label, on_label, active, color, enabled, font_size); } -auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, - int max, Color color, bool enabled, int font_size) const -> int +auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max, Color color, + bool enabled, int font_size) const -> int { // Save original styling const default_style original_default = get_default_style(); @@ -366,16 +357,15 @@ auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, in } auto user_interface::draw_menu_spinner(const int x, const int y, const int width, const int height, - const std::string& label, int* value, const int min, - const int max, const Color color, const bool enabled, - const int font_size) const -> int + const std::string& label, int* value, const int min, const int max, + const Color color, const bool enabled, const int font_size) const -> int { const Rectangle bounds = menu_grid.bounds(x, y, width, height); return draw_spinner(bounds, label, value, min, max, color, enabled, font_size); } -auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, - const bool enabled, const int font_size) const -> int +auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, const bool enabled, + const int font_size) const -> int { // Save original styling const default_style original_default = get_default_style(); @@ -405,8 +395,8 @@ auto user_interface::draw_label(const Rectangle bounds, const std::string& text, return pressed; } -auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, - const Color color, const bool enabled) const -> bool +auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, const Color color, + const bool enabled) const -> bool { component_style s = get_component_style(BUTTON); apply_block_color(s, color); @@ -452,18 +442,19 @@ auto user_interface::window_open() const -> bool auto user_interface::draw_menu_header(const Color color) const -> void { int preset = static_cast(state.get_current_preset()); - draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, - static_cast(state.get_preset_count()), color, !input.editing); + draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, static_cast(state.get_preset_count()), color, + !input.editing); if (preset > static_cast(state.get_current_preset())) { input.load_next_preset(); } else if (preset < static_cast(state.get_current_preset())) { input.load_previous_preset(); } - draw_menu_button(1, 0, 1, 1, - std::format("Puzzle: \"{}\"{}", state.get_current_preset_comment().substr(2), - state.was_edited() ? " (Modified)" : ""), - color); + draw_menu_button(1, 0, 1, 1, std::format("{}: {}/{} Blocks", + state.was_edited() + ? "Modified" + : std::format("\"{}\"", state.get_current_preset_comment().substr(2)), + state.get_current_state().block_count(), puzzle::MAX_BLOCKS), color); int editing = input.editing; draw_menu_toggle_slider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", &editing, color); @@ -474,18 +465,13 @@ auto user_interface::draw_menu_header(const Color color) const -> void auto user_interface::draw_graph_info(const Color color) const -> void { - draw_menu_button(0, 1, 1, 1, - std::format("Found {} States ({} Winning)", state.get_state_count(), - state.get_target_count()), - color); + draw_menu_button(0, 1, 1, 1, std::format("Found {} States ({} Winning)", state.get_state_count(), + state.get_target_count()), color); - draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), - color); + draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), color); - draw_menu_button(2, 1, 1, 1, - std::format("{} Moves to Nearest Solution", - state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), - color); + draw_menu_button(2, 1, 1, 1, std::format("{} Moves to Nearest Solution", + state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), color); } auto user_interface::draw_graph_controls(const Color color) const -> void @@ -506,8 +492,7 @@ auto user_interface::draw_graph_controls(const Color color) const -> void } int mark_solutions = input.mark_solutions; - draw_menu_toggle_slider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)", - &mark_solutions, color); + draw_menu_toggle_slider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)", &mark_solutions, color); if (mark_solutions != input.mark_solutions) { input.toggle_mark_solutions(); } @@ -517,22 +502,20 @@ auto user_interface::draw_graph_controls(const Color color) const -> void auto user_interface::draw_camera_controls(const Color color) const -> void { int lock_camera = input.camera_lock; - draw_menu_toggle_slider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", &lock_camera, - color); + draw_menu_toggle_slider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", &lock_camera, color); if (lock_camera != input.camera_lock) { input.toggle_camera_lock(); } int lock_camera_mass_center = input.camera_mass_center_lock; - draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", - &lock_camera_mass_center, color, input.camera_lock); + draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", &lock_camera_mass_center, color, + input.camera_lock); if (lock_camera_mass_center != input.camera_mass_center_lock) { input.toggle_camera_mass_center_lock(); } int projection = camera.projection == CAMERA_ORTHOGRAPHIC; - draw_menu_toggle_slider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", &projection, - color); + draw_menu_toggle_slider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", &projection, color); if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) { input.toggle_camera_projection(); } @@ -558,10 +541,8 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void }; const int visits = state.get_current_visits(); - draw_menu_button(0, 4, 1, 1, - std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), - visits, nth(visits)), - color); + draw_menu_button(0, 4, 1, 1, std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits, + nth(visits)), color); if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) { input.goto_optimal_next_state(); @@ -600,43 +581,40 @@ auto user_interface::draw_edit_controls(const Color color) const -> void } // Toggle Restricted/Free Block Movement - int free = !current.restricted; + int free = !current.get_restricted(); draw_menu_toggle_slider(1, 4, 1, 1, "Restricted (F)", "Free (F)", &free, color); - if (free != !current.restricted) { + if (free != !current.get_restricted()) { input.toggle_restricted_movement(); } // Clear Goal - if (draw_menu_button(1, 5, 1, 1, "Clear Goal (X)", color)) { - } + if (draw_menu_button(1, 5, 1, 1, "Clear Goal (X)", color)) {} // Column Count Spinner - int columns = current.width; + int columns = current.get_width(); draw_menu_spinner(2, 4, 1, 1, "Cols: ", &columns, puzzle::MIN_WIDTH, puzzle::MAX_WIDTH, color); - if (columns > current.width) { + if (columns > current.get_width()) { input.add_board_column(); - } else if (columns < current.width) { + } else if (columns < current.get_width()) { input.remove_board_column(); } // Row Count Spinner - int rows = current.height; + int rows = current.get_height(); draw_menu_spinner(2, 5, 1, 1, "Rows: ", &rows, puzzle::MIN_WIDTH, puzzle::MAX_WIDTH, color); - if (rows > current.height) { + if (rows > current.get_height()) { input.add_board_row(); - } else if (rows < current.height) { + } else if (rows < current.get_height()) { input.remove_board_row(); } } auto user_interface::draw_menu_footer(const Color color) -> void { - draw_menu_button(0, 6, 2, 1, std::format("State: \"{}\"", state.get_current_state().state), - color); + draw_menu_button(0, 6, 2, 1, state.get_current_state().string_repr().data(), color); if (draw_menu_button(2, 6, 1, 1, "Save as Preset", color)) { - if (const std::optional& reason = - state.get_current_state().try_get_invalid_reason()) { + if (const std::optional& reason = state.get_current_state().try_get_invalid_reason()) { message_title = "Can't Save Preset"; message_message = std::format("Invalid Board: {}.", *reason); ok_message = true; @@ -651,8 +629,7 @@ auto user_interface::get_background_color() -> Color return GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); } -auto user_interface::help_popup() -> void -{} +auto user_interface::help_popup() -> void {} auto user_interface::draw_save_preset_popup() -> void { @@ -661,8 +638,8 @@ auto user_interface::draw_save_preset_popup() -> void } // Returns the pressed button index - const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name", - "Ok;Cancel", preset_name.data(), 255, nullptr); + const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name", "Ok;Cancel", + preset_name.data(), 255, nullptr); if (button == 1) { state.append_preset_file(preset_name.data()); } @@ -678,8 +655,7 @@ auto user_interface::draw_ok_message_box() -> void return; } - const int button = - GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Ok"); + const int button = GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Ok"); if (button == 0 || button == 1) { message_title = ""; message_message = ""; @@ -693,8 +669,7 @@ auto user_interface::draw_yes_no_message_box() -> void return; } - const int button = - GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Yes;No"); + const int button = GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Yes;No"); if (button == 1) { yes_no_handler(); } @@ -727,37 +702,36 @@ auto user_interface::draw_puzzle_board() -> void { const puzzle& current = state.get_current_state(); - board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, - current.width, current.height); + board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, current.get_width(), + current.get_height()); // Draw outer border const Rectangle bounds = board_grid.square_bounds(); - DrawRectangleRec(bounds, current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED); + DrawRectangleRec(bounds, current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED); // Draw inner borders - DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, - bounds.width - 2 * BOARD_PADDING, bounds.height - 2 * BOARD_PADDING, - current.restricted ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE); + DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, bounds.width - 2 * BOARD_PADDING, + bounds.height - 2 * BOARD_PADDING, + current.get_restricted() ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE); // Draw target opening // TODO: Only draw single direction (in corner) if restricted (use target block principal // direction) const std::optional target_block = current.try_get_target_block(); - const int target_x = current.target_x; - const int target_y = current.target_y; - if (current.has_win_condition() && target_block) { - auto [x, y, width, height] = board_grid.square_bounds( - target_x, target_y, target_block.value().width, target_block.value().height); + const int target_x = current.get_goal_x(); + const int target_y = current.get_goal_y(); + if (current.get_goal() && target_block) { + auto [x, y, width, height] = board_grid.square_bounds(target_x, target_y, target_block->get_width(), + target_block->get_height()); - const Color opening_color = - Fade(current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3); + const Color opening_color = Fade(current.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3); if (target_x == 0) { // Left opening DrawRectangle(x - BOARD_PADDING, y, BOARD_PADDING, height, RAYWHITE); DrawRectangle(x - BOARD_PADDING, y, BOARD_PADDING, height, opening_color); } - if (target_x + target_block.value().width == current.width) { + if (target_x + target_block->get_width() == current.get_width()) { // Right opening DrawRectangle(x + width, y, BOARD_PADDING, height, RAYWHITE); DrawRectangle(x + width, y, BOARD_PADDING, height, opening_color); @@ -767,7 +741,7 @@ auto user_interface::draw_puzzle_board() -> void DrawRectangle(x, y - BOARD_PADDING, width, BOARD_PADDING, RAYWHITE); DrawRectangle(x, y - BOARD_PADDING, width, BOARD_PADDING, opening_color); } - if (target_y + target_block.value().height == current.height) { + if (target_y + target_block->get_height() == current.get_height()) { // Bottom opening DrawRectangle(x, y + height, width, BOARD_PADDING, RAYWHITE); DrawRectangle(x, y + height, width, BOARD_PADDING, opening_color); @@ -794,87 +768,87 @@ auto user_interface::draw_puzzle_board() -> void } // Draw blocks - for (const puzzle::block& b : current) { + for (const puzzle::block b : current.block_view()) { Color c = BLOCK_COLOR; - if (b.target) { + if (b.get_target()) { c = TARGET_BLOCK_COLOR; - } else if (b.immovable) { + } else if (b.get_immovable()) { c = WALL_COLOR; } - if (!b.valid() || b.x < 0 || b.y < 0 || b.width <= 0 || b.height <= 0) { - warnln("Iterator returned invalid block for state \"{}\".", current.state); - continue; - } + const auto [x, y, w, h, t, i] = b.unpack_repr(); - draw_board_block(b.x, b.y, b.width, b.height, c, !b.immovable); + draw_board_block(x, y, w, h, c, !i); } // Draw block placing if (input.editing && input.has_block_add_xy) { - if (current.covers(input.block_add_x, input.block_add_y) && - input.hov_x >= input.block_add_x && input.hov_y >= input.block_add_y) { + if (current.covers(input.block_add_x, input.block_add_y) && input.hov_x >= input.block_add_x && input.hov_y >= + input.block_add_y) { bool collides = false; - for (const puzzle::block& b : current) { - if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, - input.hov_x - input.block_add_x + 1, + for (const puzzle::block b : current.block_view()) { + if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1, input.hov_y - input.block_add_y + 1, false))) { collides = true; break; } } if (!collides) { - draw_board_block(input.block_add_x, input.block_add_y, - input.hov_x - input.block_add_x + 1, + draw_board_block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1, input.hov_y - input.block_add_y + 1, PURPLE); } } } + // TODO: In edit mode + // - Clear Goal button doesn't work + // - Toggle Target Block button throws "Grid bounds out of range" + // - Clicking the goal to remove it throws "Grid bounds out of range" // Draw goal boundaries when editing - if (input.editing) { - DrawRectangleLinesEx(board_grid.square_bounds(target_x, target_y, target_block->width, target_block->height), 2.0, TARGET_BLOCK_COLOR); + if (input.editing && current.get_goal() && target_block) { + DrawRectangleLinesEx( + board_grid.square_bounds(target_x, target_y, target_block->get_width(), target_block->get_height()), 2.0, + TARGET_BLOCK_COLOR); } } -auto user_interface::draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) - -> void +auto user_interface::draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> void { graph_overlay_grid.update_bounds(GetScreenWidth() / 2, MENU_HEIGHT); debug_overlay_grid.update_bounds(GetScreenWidth() / 2, GetScreenHeight() - 75); - draw_label(graph_overlay_grid.bounds(0, 0, 1, 1), - std::format("Dist: {:0>7.2f}", camera.distance), BLACK); - draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov), - BLACK); + draw_label(graph_overlay_grid.bounds(0, 0, 1, 1), std::format("Dist: {:0>7.2f}", camera.distance), BLACK); + draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov), BLACK); draw_label(graph_overlay_grid.bounds(0, 2, 1, 1), std::format("FPS: {:0>3}", fps), LIME); draw_label(graph_overlay_grid.bounds(0, 3, 1, 1), std::format("UPS: {:0>3}", ups), ORANGE); // Debug draw_label(debug_overlay_grid.bounds(0, 0, 1, 1), std::format("Physics Debug:"), BLACK); - draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count), - BLACK); - draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count), - BLACK); + draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count), BLACK); + draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count), BLACK); } -auto user_interface::draw(const int fps, const int ups, const size_t mass_count, - const size_t spring_count) -> void +auto user_interface::draw(const int fps, const int ups, const size_t mass_count, const size_t spring_count) -> void { - const auto visitor = overloads{[&](const show_ok_message& msg) - { - message_title = msg.title; - message_message = msg.message; - ok_message = true; - }, - [&](const show_yes_no_message& msg) - { - message_title = msg.title; - message_message = msg.message; - yes_no_handler = msg.on_yes; - yes_no_message = true; - }, - [&](const show_save_preset_window& msg) { save_window = true; }}; + const auto visitor = overloads{ + [&](const show_ok_message& msg) + { + message_title = msg.title; + message_message = msg.message; + ok_message = true; + }, + [&](const show_yes_no_message& msg) + { + message_title = msg.title; + message_message = msg.message; + yes_no_handler = msg.on_yes; + yes_no_message = true; + }, + [&](const show_save_preset_window& msg) + { + save_window = true; + } + }; while (!input.ui_commands.empty()) { const ui_command& cmd = input.ui_commands.front(); diff --git a/test/bits.cpp b/test/bits.cpp new file mode 100644 index 0000000..e7141bc --- /dev/null +++ b/test/bits.cpp @@ -0,0 +1,267 @@ +#include +#include +#include + +#include "util.hpp" + +// ============================================================================= +// Catch2 +// ============================================================================= +// +// 1. TEST_CASE(name, tags) +// The basic unit of testing in Catch2. Each TEST_CASE is an independent test +// function. The first argument is a descriptive name (must be unique), and +// the second is a string of tags in square brackets (e.g. "[set_bits]") +// used to filter and group tests when running. +// +// 2. SECTION(name) +// Sections allow multiple subtests within a single TEST_CASE. Each SECTION +// runs the TEST_CASE from the top, so any setup code before the SECTION is +// re-executed fresh for every section. This gives each section an isolated +// starting state without needing separate TEST_CASEs or explicit teardown. +// Sections can also be nested. +// +// 3. REQUIRE(expression) +// The primary assertion macro. If the expression evaluates to false, the +// test fails immediately and Catch2 reports the actual values of both sides +// of the comparison (e.g. "0xF5 == 0xF0" on failure). There is also +// CHECK(), which records a failure but continues executing the rest of the +// test; REQUIRE() aborts the current test on failure. +// +// 4. TEMPLATE_TEST_CASE(name, tags, Type1, Type2, ...) +// A parameterised test that is instantiated once for each type listed. +// Inside the test body, the alias `TestType` refers to the current type. +// This avoids duplicating identical logic for uint8_t, uint16_t, uint32_t, +// and uint64_t. Catch2 automatically appends the type name to the test name +// in the output so you can see which instantiation failed. +// +// 5. Tags (e.g. "[create_mask]", "[round-trip]") +// Tags let you selectively run subsets of tests from the command line. +// For example: +// ./tests "[set_bits]" -- runs only tests tagged [set_bits] +// ./tests "~[round-trip]" -- runs everything except [round-trip] +// ./tests "[get_bits],[set_bits]" -- runs tests matching either tag +// +// ============================================================================= + +// --------------------------------------------------------------------------- +// create_mask +// --------------------------------------------------------------------------- + +TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]", + uint8_t, uint16_t, uint32_t, uint64_t) +{ + SECTION("single bit mask at bit 0") { + auto m = create_mask(0, 0); + REQUIRE(m == TestType{0b1}); + } + + SECTION("single bit mask at bit 3") { + auto m = create_mask(3, 3); + REQUIRE(m == TestType{0b1000}); + } + + SECTION("mask spanning bits 0..7 gives 0xFF") { + auto m = create_mask(0, 7); + REQUIRE(m == TestType{0xFF}); + } + + SECTION("mask spanning bits 4..7") { + auto m = create_mask(4, 7); + REQUIRE(m == TestType{0xF0}); + } + + SECTION("full-width mask returns all ones") { + constexpr uint8_t last = sizeof(TestType) * 8 - 1; + auto m = create_mask(0, last); + REQUIRE(m == static_cast(~TestType{0})); + } +} + +TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") { + REQUIRE(create_mask(0, 15) == 0x0000FFFF); + REQUIRE(create_mask(0, 31) == 0xFFFFFFFF); + REQUIRE(create_mask(16, 31) == 0xFFFF0000); +} + +// --------------------------------------------------------------------------- +// clear_bits +// --------------------------------------------------------------------------- + +TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]", + uint8_t, uint16_t, uint32_t, uint64_t) +{ + SECTION("clear all bits") { + TestType val = static_cast(~TestType{0}); + constexpr uint8_t last = sizeof(TestType) * 8 - 1; + clear_bits(val, 0, last); + REQUIRE(val == TestType{0}); + } + + SECTION("clear lower nibble") { + TestType val = static_cast(0xFF); + clear_bits(val, 0, 3); + REQUIRE(val == static_cast(0xF0)); + } + + SECTION("clear upper nibble") { + TestType val = static_cast(0xFF); + clear_bits(val, 4, 7); + REQUIRE(val == static_cast(0x0F)); + } + + SECTION("clear single bit") { + TestType val = static_cast(0xFF); + clear_bits(val, 0, 0); + REQUIRE(val == static_cast(0xFE)); + } + + SECTION("clearing already-zero bits is a no-op") { + TestType val = TestType{0}; + clear_bits(val, 0, 3); + REQUIRE(val == TestType{0}); + } +} + +// --------------------------------------------------------------------------- +// set_bits +// --------------------------------------------------------------------------- + +TEMPLATE_TEST_CASE("set_bits writes value into the specified range", "[set_bits]", + uint8_t, uint16_t, uint32_t, uint64_t) +{ + SECTION("set lower nibble on zero") { + TestType val = TestType{0}; + set_bits(val, uint8_t{0}, uint8_t{3}, static_cast(0xA)); + REQUIRE(val == static_cast(0x0A)); + } + + SECTION("set upper nibble on zero") { + TestType val = TestType{0}; + set_bits(val, uint8_t{4}, uint8_t{7}, static_cast(0xB)); + REQUIRE(val == static_cast(0xB0)); + } + + SECTION("set_bits replaces existing bits") { + TestType val = static_cast(0xFF); + set_bits(val, uint8_t{0}, uint8_t{3}, static_cast(0x5)); + REQUIRE(val == static_cast(0xF5)); + } + + SECTION("set single bit to 1") { + TestType val = TestType{0}; + set_bits(val, uint8_t{3}, uint8_t{3}, static_cast(1)); + REQUIRE(val == static_cast(0x08)); + } + + SECTION("set single bit to 0") { + TestType val = static_cast(0xFF); + set_bits(val, uint8_t{3}, uint8_t{3}, static_cast(0)); + REQUIRE(val == static_cast(0xF7)); + } + + SECTION("setting value 0 clears the range") { + TestType val = static_cast(0xFF); + set_bits(val, uint8_t{0}, uint8_t{7}, static_cast(0)); + REQUIRE(val == TestType{0}); + } +} + +TEST_CASE("set_bits with different value type (U != T)", "[set_bits]") { + uint32_t val = 0; + constexpr uint8_t small_val = 0x3F; + set_bits(val, uint8_t{8}, uint8_t{13}, small_val); + REQUIRE(val == (uint32_t{0x3F} << 8)); +} + +TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") { + uint32_t val = 0xDEADBEEF; + set_bits(val, uint8_t{8}, uint8_t{15}, uint32_t{0x42}); + REQUIRE(val == 0xDEAD42EF); +} + +// --------------------------------------------------------------------------- +// get_bits +// --------------------------------------------------------------------------- + +TEMPLATE_TEST_CASE("get_bits extracts the specified range", "[get_bits]", + uint8_t, uint16_t, uint32_t, uint64_t) +{ + SECTION("get lower nibble") { + TestType val = static_cast(0xAB); + auto result = get_bits(val, uint8_t{0}, uint8_t{3}); + REQUIRE(result == TestType{0xB}); + } + + SECTION("get upper nibble") { + TestType val = static_cast(0xAB); + auto result = get_bits(val, uint8_t{4}, uint8_t{7}); + REQUIRE(result == TestType{0xA}); + } + + SECTION("get single bit that is set") { + TestType val = static_cast(0x08); + auto result = get_bits(val, uint8_t{3}, uint8_t{3}); + REQUIRE(result == TestType{1}); + } + + SECTION("get single bit that is clear") { + TestType val = static_cast(0xF7); + auto result = get_bits(val, uint8_t{3}, uint8_t{3}); + REQUIRE(result == TestType{0}); + } + + SECTION("get all bits") { + TestType val = static_cast(~TestType{0}); + constexpr uint8_t last = sizeof(TestType) * 8 - 1; + auto result = get_bits(val, uint8_t{0}, last); + REQUIRE(result == val); + } + + SECTION("get from zero returns zero") { + TestType val = TestType{0}; + auto result = get_bits(val, uint8_t{0}, uint8_t{7}); + REQUIRE(result == TestType{0}); + } +} + +TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") { + constexpr uint32_t val = 0xDEADBEEF; + + REQUIRE(get_bits(val, uint8_t{0}, uint8_t{7}) == 0xEF); + REQUIRE(get_bits(val, uint8_t{8}, uint8_t{15}) == 0xBE); + REQUIRE(get_bits(val, uint8_t{16}, uint8_t{23}) == 0xAD); + REQUIRE(get_bits(val, uint8_t{24}, uint8_t{31}) == 0xDE); +} + +// --------------------------------------------------------------------------- +// Round-trip: set then get +// --------------------------------------------------------------------------- + +TEST_CASE("set_bits then get_bits round-trips correctly", "[round-trip]") { + uint32_t reg = 0; + + set_bits(reg, uint8_t{4}, uint8_t{11}, uint32_t{0xAB}); + REQUIRE(get_bits(reg, uint8_t{4}, uint8_t{11}) == 0xAB); + + REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{3}) == 0x0); + REQUIRE(get_bits(reg, uint8_t{12}, uint8_t{31}) == 0x0); +} + +TEST_CASE("multiple set_bits on different ranges", "[round-trip]") { + uint32_t reg = 0; + + set_bits(reg, uint8_t{0}, uint8_t{7}, uint32_t{0x01}); + set_bits(reg, uint8_t{8}, uint8_t{15}, uint32_t{0x02}); + set_bits(reg, uint8_t{16}, uint8_t{23}, uint32_t{0x03}); + set_bits(reg, uint8_t{24}, uint8_t{31}, uint32_t{0x04}); + + REQUIRE(reg == 0x04030201); +} + +TEST_CASE("64-bit round-trip", "[round-trip]") { + uint64_t reg = 0; + set_bits(reg, uint8_t{32}, uint8_t{63}, uint64_t{0xCAFEBABE}); + REQUIRE(get_bits(reg, uint8_t{32}, uint8_t{63}) == uint64_t{0xCAFEBABE}); + REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{31}) == uint64_t{0}); +} \ No newline at end of file