From 3230d806f73e29d64ffa6da28d12f0fe94375882 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Sat, 7 Mar 2026 23:33:55 +0100 Subject: [PATCH] restructure puzzle space generation (for boards up to 5x5) - uses a huge global seen-states-cache. Not scalable without more filtering --- CMakeLists.txt | 4 +- benchmark/state_space.cpp | 70 +-- flake.nix | 2 +- include/bits.hpp | 10 +- include/config.hpp | 1 + include/cpu_layout_engine.hpp | 15 +- include/cpu_spring_system.hpp | 27 +- include/graph_distances.hpp | 6 +- include/input_handler.hpp | 23 +- include/octree.hpp | 44 +- include/orbit_camera.hpp | 3 +- include/puzzle.hpp | 715 ++++++++++++++++++------------- include/renderer.hpp | 5 +- include/state_manager.hpp | 16 +- include/user_interface.hpp | 105 +++-- include/util.hpp | 43 +- src/bits.cpp | 8 +- src/cpu_layout_engine.cpp | 17 +- src/cpu_spring_system.cpp | 56 +-- src/graph_distances.cpp | 2 +- src/input_handler.cpp | 4 +- src/load_save.cpp | 5 +- src/main.cpp | 224 ++++++---- src/octree.cpp | 12 +- src/orbit_camera.cpp | 23 +- src/puzzle.cpp | 632 +++++++++++++-------------- src/renderer.cpp | 8 +- src/state_manager.cpp | 4 +- src/user_interface.cpp | 262 ++++++++--- test/bitmap.cpp | 20 +- test/bitmap_find_first_empty.cpp | 34 +- test/bits.cpp | 98 ++--- test/puzzle.cpp | 304 ++++++------- 33 files changed, 1557 insertions(+), 1245 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3446b1e..e58723c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,7 +113,7 @@ if(NOT DISABLE_TESTS AND NOT WIN32) add_executable(tests ${TEST_SOURCES} ${SOURCES}) target_include_directories(tests PRIVATE include) - target_link_libraries(tests Catch2::Catch2WithMain raylib) + target_link_libraries(tests Catch2::Catch2WithMain raylib GLEW::GLEW) include(Catch) catch_discover_tests(tests) @@ -131,7 +131,7 @@ if(NOT DISABLE_BENCH AND NOT WIN32) add_executable(benchmarks ${BENCH_SOURCES} ${SOURCES}) target_include_directories(benchmarks PRIVATE include) - target_link_libraries(benchmarks benchmark raylib) + target_link_libraries(benchmarks benchmark raylib GLEW::GLEW) message("-- BENCHMARKS: Enabled") endif() diff --git a/benchmark/state_space.cpp b/benchmark/state_space.cpp index 62a7630..4057560 100644 --- a/benchmark/state_space.cpp +++ b/benchmark/state_space.cpp @@ -1,4 +1,5 @@ // ReSharper disable CppTooWideScope +// ReSharper disable CppDFAUnreadVariable #include "puzzle.hpp" #include @@ -39,12 +40,12 @@ static std::vector puzzles = { "S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]", }; -template +template struct uint_hasher { int64_t nums; - auto operator()(const std::array& ints) const noexcept -> size_t + auto operator()(const std::array& ints) const noexcept -> size_t { size_t h = 0; for (size_t i = 0; i < N; ++i) { @@ -54,18 +55,18 @@ struct uint_hasher } }; -template +template static auto unordered_set_uint64(benchmark::State& state) -> void { std::random_device random_device; std::mt19937 generator(random_device()); - std::uniform_int_distribution distribution( - std::numeric_limits::min(), - std::numeric_limits::max() + std::uniform_int_distribution distribution( + std::numeric_limits::min(), + std::numeric_limits::max() ); - std::unordered_set, uint_hasher> set; - std::array ints; + std::unordered_set, uint_hasher> set; + std::array ints; for (size_t i = 0; i < N; ++i) { ints[i] = distribution(generator); } @@ -79,18 +80,18 @@ static auto unordered_set_uint64(benchmark::State& state) -> void } } -template +template static auto unordered_flat_set_uint64(benchmark::State& state) -> void { std::random_device random_device; std::mt19937 generator(random_device()); - std::uniform_int_distribution distribution( - std::numeric_limits::min(), - std::numeric_limits::max() + std::uniform_int_distribution distribution( + std::numeric_limits::min(), + std::numeric_limits::max() ); - boost::unordered_flat_set, uint_hasher> set; - std::array ints; + boost::unordered_flat_set, uint_hasher> set; + std::array ints; for (size_t i = 0; i < N; ++i) { ints[i] = distribution(generator); } @@ -106,8 +107,8 @@ static auto unordered_flat_set_uint64(benchmark::State& state) -> void static auto unordered_flat_set_block_hasher(benchmark::State& state) -> void { - boost::unordered_flat_set set; - const puzzle::block b = puzzle::block(2, 3, 1, 2, true, false); + blockset set; + const block b = block(2, 3, 1, 2, true, false); for (auto _ : state) { for (size_t i = 0; i < 100000; ++i) { @@ -118,10 +119,10 @@ static auto unordered_flat_set_block_hasher(benchmark::State& state) -> void } } -static auto unordered_falt_set_block_hasher2(benchmark::State& state) -> void +static auto unordered_flat_set_block_hasher2(benchmark::State& state) -> void { - boost::unordered_flat_set set; - const puzzle::block b = puzzle::block(2, 3, 1, 2, true, false); + blockset2 set; + const block b = block(2, 3, 1, 2, true, false); for (auto _ : state) { for (size_t i = 0; i < 100000; ++i) { @@ -134,7 +135,7 @@ static auto unordered_falt_set_block_hasher2(benchmark::State& state) -> void static auto unordered_flat_set_puzzle_hasher(benchmark::State& state) -> void { - boost::unordered_flat_set set; + puzzleset set; const puzzle p = puzzle(puzzles[0]); for (auto _ : state) { @@ -159,22 +160,22 @@ static auto explore_state_space(benchmark::State& state) -> void static auto explore_rush_hour_puzzle_space(benchmark::State& state) -> void { - constexpr uint8_t max_blocks = 5; + constexpr u8 max_blocks = 5; - constexpr uint8_t board_width = 4; - constexpr uint8_t board_height = 5; - constexpr uint8_t goal_x = board_width - 1; - constexpr uint8_t goal_y = 2; + constexpr u8 board_width = 4; + constexpr u8 board_height = 5; + constexpr u8 goal_x = board_width - 1; + constexpr u8 goal_y = 2; constexpr bool restricted = true; - const boost::unordered_flat_set permitted_blocks = { - puzzle::block(0, 0, 2, 1, false, false), - puzzle::block(0, 0, 3, 1, false, false), - puzzle::block(0, 0, 1, 2, false, false), - puzzle::block(0, 0, 1, 3, false, false) + const blockset2 permitted_blocks = { + block(0, 0, 2, 1, false, false), + block(0, 0, 3, 1, false, false), + block(0, 0, 1, 2, false, false), + block(0, 0, 1, 3, false, false) }; - const puzzle::block target_block = puzzle::block(0, 0, 2, 1, true, false); - constexpr std::tuple target_block_pos_range = { + const block target_block = block(0, 0, 2, 1, true, false); + constexpr std::tuple target_block_pos_range = { 0, goal_y, goal_x, @@ -184,11 +185,12 @@ static auto explore_rush_hour_puzzle_space(benchmark::State& state) -> void const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true); for (auto _ : state) { - boost::unordered_flat_set result = p.explore_puzzle_space( + puzzleset result = p.explore_puzzle_space( permitted_blocks, target_block, target_block_pos_range, max_blocks, + 0, std::nullopt); benchmark::DoNotOptimize(result); @@ -202,7 +204,7 @@ BENCHMARK(unordered_flat_set_uint64<4>)->Unit(benchmark::kMicrosecond); BENCHMARK(unordered_flat_set_uint64<8>)->Unit(benchmark::kMicrosecond); BENCHMARK(unordered_flat_set_uint64<16>)->Unit(benchmark::kMicrosecond); BENCHMARK(unordered_flat_set_block_hasher)->Unit(benchmark::kMicrosecond); -BENCHMARK(unordered_falt_set_block_hasher2)->Unit(benchmark::kMicrosecond); +BENCHMARK(unordered_flat_set_block_hasher2)->Unit(benchmark::kMicrosecond); BENCHMARK(unordered_flat_set_puzzle_hasher)->Unit(benchmark::kMicrosecond); BENCHMARK(explore_state_space)->DenseRange(0, puzzles.size() - 1)->Unit(benchmark::kMicrosecond); BENCHMARK(explore_rush_hour_puzzle_space)->Unit(benchmark::kSecond); diff --git a/flake.nix b/flake.nix index 452c12c..eb6e2ad 100644 --- a/flake.nix +++ b/flake.nix @@ -111,7 +111,7 @@ rec { abbr -a release "${buildRelease} && ./cmake-build-release/masssprings" abbr -a run "${buildRelease} && ./cmake-build-release/masssprings" - abbr -a run-clusters "${buildRelease} && ./cmake-build-release/masssprings --output=clusters.puzzle --space=rh --w=6 --h=6 --gx=4 --gy=2 --blocks=4" + abbr -a runclusters "${buildRelease} && ./cmake-build-release/masssprings --output=clusters.puzzle --space=rh --moves=10 --blocks=4" abbr -a runtests "${buildDebug} && ./cmake-build-debug/tests" abbr -a runbenchs "mv -f benchs.json benchs.old.json; ${buildRelease} && sudo cpupower frequency-set --governor performance && ./cmake-build-release/benchmarks --benchmark_out=benchs.json --benchmark_out_format=console; sudo cpupower frequency-set --governor powersave" abbr -a rungdb "${buildDebug} && gdb --tui ./cmake-build-debug/masssprings" diff --git a/include/bits.hpp b/include/bits.hpp index 994606c..e3d4256 100644 --- a/include/bits.hpp +++ b/include/bits.hpp @@ -8,7 +8,7 @@ template requires std::unsigned_integral // ReSharper disable once CppRedundantInlineSpecifier -INLINE inline auto create_mask(const uint8_t first, const uint8_t last) -> T +INLINE inline auto create_mask(const u8 first, const u8 last) -> T { // If the mask width is equal the type width return all 1s instead of shifting // as shifting by type-width is undefined behavior. @@ -27,7 +27,7 @@ INLINE inline auto create_mask(const uint8_t first, const uint8_t last) -> T template requires std::unsigned_integral // ReSharper disable once CppRedundantInlineSpecifier -INLINE inline auto clear_bits(T& bits, const uint8_t first, const uint8_t last) -> void +INLINE inline auto clear_bits(T& bits, const u8 first, const u8 last) -> void { const T mask = create_mask(first, last); @@ -37,7 +37,7 @@ INLINE inline auto clear_bits(T& bits, const uint8_t first, const uint8_t last) template requires std::unsigned_integral && std::unsigned_integral // ReSharper disable once CppRedundantInlineSpecifier -INLINE inline auto set_bits(T& bits, const uint8_t first, const uint8_t last, const U value) -> void +INLINE inline auto set_bits(T& bits, const u8 first, const u8 last, const U value) -> void { const T mask = create_mask(first, last); @@ -56,7 +56,7 @@ INLINE inline auto set_bits(T& bits, const uint8_t first, const uint8_t last, co template requires std::unsigned_integral // ReSharper disable once CppRedundantInlineSpecifier -INLINE inline auto get_bits(const T bits, const uint8_t first, const uint8_t last) -> T +INLINE inline auto get_bits(const T bits, const u8 first, const u8 last) -> T { const T mask = create_mask(first, last); @@ -64,6 +64,6 @@ INLINE inline auto get_bits(const T bits, const uint8_t first, const uint8_t las return (bits & mask) >> first; } -auto print_bitmap(uint64_t bitmap, uint8_t w, uint8_t h, const std::string& title) -> void; +auto print_bitmap(u64 bitmap, u8 w, u8 h, const std::string& title) -> void; #endif \ No newline at end of file diff --git a/include/config.hpp b/include/config.hpp index 3a8bc29..3105d33 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -23,6 +23,7 @@ #define BS_THREAD_POOL_NATIVE_EXTENSIONS // ReSharper disable once CppUnusedIncludeDirective #include +using threadpool = std::optional* const>; #if defined(_WIN32) // raylib uses these names as function parameters #undef near #undef far diff --git a/include/cpu_layout_engine.hpp b/include/cpu_layout_engine.hpp index edfe346..f7feda2 100644 --- a/include/cpu_layout_engine.hpp +++ b/include/cpu_layout_engine.hpp @@ -2,6 +2,8 @@ #define PHYSICS_HPP_ #include "config.hpp" +#include "cpu_spring_system.hpp" +#include "util.hpp" #include #include @@ -57,7 +59,7 @@ class cpu_layout_engine }; private: - std::optional* const> thread_pool; + threadpool thread_pool; std::thread physics; public: @@ -65,14 +67,11 @@ public: public: explicit cpu_layout_engine( - const std::optional* const> _thread_pool = std::nullopt) + const threadpool _thread_pool = std::nullopt) : thread_pool(_thread_pool), physics(physics_thread, std::ref(state), std::ref(thread_pool)) {} - cpu_layout_engine(const cpu_layout_engine& copy) = delete; - auto operator=(const cpu_layout_engine& copy) -> cpu_layout_engine& = delete; - cpu_layout_engine(cpu_layout_engine&& move) = delete; - auto operator=(cpu_layout_engine&& move) -> cpu_layout_engine& = delete; + NO_COPY_NO_MOVE(cpu_layout_engine); ~cpu_layout_engine() { @@ -88,14 +87,14 @@ private: #endif static auto physics_thread(physics_state& state, - std::optional* const> thread_pool) -> void; + threadpool thread_pool) -> void; public: auto clear_cmd() -> void; auto add_mass_cmd() -> void; auto add_spring_cmd(size_t a, size_t b) -> void; auto add_mass_springs_cmd(size_t num_masses, - const std::vector>& springs) -> void; + const std::vector& springs) -> void; }; #endif \ No newline at end of file diff --git a/include/cpu_spring_system.hpp b/include/cpu_spring_system.hpp index 6707183..0bba8e2 100644 --- a/include/cpu_spring_system.hpp +++ b/include/cpu_spring_system.hpp @@ -7,20 +7,10 @@ #include #include +using spring = std::pair; + class cpu_spring_system { -public: - class spring - { - public: - size_t a; - size_t b; - - public: - spring(const size_t _a, const size_t _b) - : a(_a), b(_b) {} - }; - public: octree tree; @@ -35,10 +25,7 @@ public: public: cpu_spring_system() {} - cpu_spring_system(const cpu_spring_system& copy) = delete; - auto operator=(const cpu_spring_system& copy) -> cpu_spring_system& = delete; - cpu_spring_system(cpu_spring_system& move) = delete; - auto operator=(cpu_spring_system&& move) -> cpu_spring_system& = delete; + NO_COPY_NO_MOVE(cpu_spring_system); public: auto clear() -> void; @@ -47,14 +34,14 @@ public: auto clear_forces() -> void; auto calculate_spring_force(size_t s) -> void; - auto calculate_spring_forces(std::optional* const> thread_pool = std::nullopt) -> void; - auto calculate_repulsion_forces(std::optional* const> thread_pool = std::nullopt) -> void; + auto calculate_spring_forces(threadpool thread_pool = std::nullopt) -> void; + auto calculate_repulsion_forces(threadpool thread_pool = std::nullopt) -> void; auto integrate_velocity(size_t m, float dt) -> void; auto integrate_position(size_t m, float dt) -> void; auto verlet_update(size_t m, float dt) -> void; - auto update(float dt, std::optional* const> thread_pool = std::nullopt) -> void; + auto update(float dt, threadpool thread_pool = std::nullopt) -> void; - auto center_masses(std::optional* const> thread_pool = std::nullopt) -> void; + auto center_masses(threadpool thread_pool = std::nullopt) -> void; }; #endif \ No newline at end of file diff --git a/include/graph_distances.hpp b/include/graph_distances.hpp index 63df736..c47eef3 100644 --- a/include/graph_distances.hpp +++ b/include/graph_distances.hpp @@ -1,7 +1,8 @@ #ifndef DISTANCE_HPP_ #define DISTANCE_HPP_ -#include +#include "cpu_spring_system.hpp" + #include class graph_distances @@ -15,7 +16,8 @@ public: auto clear() -> void; [[nodiscard]] auto empty() const -> bool; - auto calculate_distances(size_t node_count, const std::vector>& edges, + auto calculate_distances(size_t node_count, + const std::vector& edges, const std::vector& targets) -> void; [[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector; diff --git a/include/input_handler.hpp b/include/input_handler.hpp index 2e38a82..262786c 100644 --- a/include/input_handler.hpp +++ b/include/input_handler.hpp @@ -22,8 +22,7 @@ struct show_yes_no_message std::function on_yes; }; -struct show_save_preset_window -{}; +struct show_save_preset_window {}; using ui_command = std::variant; @@ -91,15 +90,13 @@ public: size_t collision_mass = -1; public: - input_handler(state_manager& _state, orbit_camera& _camera) : state(_state), camera(_camera) + input_handler(state_manager& _state, orbit_camera& _camera) + : state(_state), camera(_camera) { init_handlers(); } - input_handler(const input_handler& copy) = delete; - auto operator=(const input_handler& copy) -> input_handler& = delete; - input_handler(input_handler&& move) = delete; - auto operator=(input_handler&& move) -> input_handler& = delete; + NO_COPY_NO_MOVE(input_handler); private: auto init_handlers() -> void; @@ -164,18 +161,14 @@ public: // General auto register_generic_handler(const std::function& handler) -> void; - auto register_mouse_pressed_handler(MouseButton button, - const std::function& handler) -> void; + auto register_mouse_pressed_handler(MouseButton button, const std::function& handler) -> void; auto register_mouse_released_handler(MouseButton button, - const std::function& handler) - -> void; + const std::function& handler) -> void; - auto register_key_pressed_handler(KeyboardKey key, - const std::function& handler) -> void; + auto register_key_pressed_handler(KeyboardKey key, const std::function& handler) -> void; - auto register_key_released_handler(KeyboardKey key, - const std::function& handler) -> void; + auto register_key_released_handler(KeyboardKey key, const std::function& handler) -> void; auto handle_input() -> void; }; diff --git a/include/octree.hpp b/include/octree.hpp index 2ed1595..a660e37 100644 --- a/include/octree.hpp +++ b/include/octree.hpp @@ -18,7 +18,7 @@ class octree public: Vector3 mass_center = Vector3Zero(); float mass_total = 0.0; - uint8_t depth = 0; + u8 depth = 0; float size = 0.0f; // Because our octree cells are cubic we don't need to store the bounds std::array children = {-1, -1, -1, -1, -1, -1, -1, -1}; int mass_id = -1; @@ -26,7 +26,7 @@ class octree }; private: - // 21 * 3 = 63, fits in uint64_t for combined x/y/z morton-code + // 21 * 3 = 63, fits in u64 for combined x/y/z morton-code static constexpr int MAX_DEPTH = 21; std::vector nodes; @@ -39,15 +39,13 @@ private: // std::vector box_maxs; // std::vector> childrens; // std::vector mass_ids; - // std::vector leafs; // bitpacked std::vector is a lot slower + // std::vector leafs; // bitpacked std::vector is a lot slower public: octree() = default; - // octree(const octree& copy) = delete; - // auto operator=(const octree& copy) -> octree& = delete; - // octree(octree&& move) = delete; - // auto operator=(octree&& move) -> octree& = delete; + // Required for async octree + // NO_COPY_NO_MOVE(octree); private: [[nodiscard]] INLINE static inline auto get_octant(const Vector3& box_min, @@ -62,14 +60,14 @@ private: [[nodiscard]] INLINE static inline auto quantize_axis(float coordinate, float box_min, float box_max, - int bits) -> uint32_t; + int bits) -> u32; [[nodiscard]] INLINE static inline auto pos_to_morton(const Vector3& p, const Vector3& root_min, - const Vector3& root_max) -> uint64_t; + const Vector3& root_max) -> u64; [[nodiscard]] INLINE static inline auto jitter_pos(Vector3 p, - uint32_t seed, + u32 seed, const Vector3& root_min, const Vector3& root_max, float root_extent) -> Vector3; @@ -82,7 +80,7 @@ private: // - [101 110 100] // - [101 110] // - [101] (root) - [[nodiscard]] INLINE static inline auto path_to_ancestor(uint64_t leaf_code, int leaf_depth, int depth) -> uint64_t; + [[nodiscard]] INLINE static inline auto path_to_ancestor(u64 leaf_code, int leaf_depth, int depth) -> u64; // Use this to obtain the octant a leaf node is contained in (on any level). // The 3 interleaved bits in the morten code encode the octant [0, 7]. @@ -90,7 +88,7 @@ private: // - [100] (Level 2) // - [110] (Level 1) // - [101] (Level 0) - [[nodiscard]] INLINE static inline auto octant_at_level(uint64_t leaf_code, int level, int leaf_depth) -> int; + [[nodiscard]] INLINE static inline auto octant_at_level(u64 leaf_code, int level, int leaf_depth) -> int; public: auto clear() -> void; @@ -141,7 +139,7 @@ INLINE inline auto octree::get_child_bounds(const Vector3& box_min, INLINE inline auto octree::quantize_axis(const float coordinate, const float box_min, const float box_max, - const int bits) -> uint32_t + const int bits) -> u32 { const float extent = box_max - box_min; if (extent <= 0.0f) { @@ -152,20 +150,20 @@ INLINE inline auto octree::quantize_axis(const float coordinate, normalized = std::max(0.0f, std::min(normalized, std::nextafter(1.0f, 0.0f))); // avoid exactly 1.0 // bits up to 21 => (1u << bits) safe in 32-bit - const uint32_t grid_max = (1u << bits) - 1u; - return static_cast(normalized * static_cast(grid_max)); + const u32 grid_max = (1u << bits) - 1u; + return static_cast(normalized * static_cast(grid_max)); } -INLINE inline auto octree::pos_to_morton(const Vector3& p, const Vector3& root_min, const Vector3& root_max) -> uint64_t +INLINE inline auto octree::pos_to_morton(const Vector3& p, const Vector3& root_min, const Vector3& root_max) -> u64 { - const uint32_t x = quantize_axis(p.x, root_min.x, root_max.x, MAX_DEPTH); - const uint32_t y = quantize_axis(p.y, root_min.y, root_max.y, MAX_DEPTH); - const uint32_t z = quantize_axis(p.z, root_min.z, root_max.z, MAX_DEPTH); + const u32 x = quantize_axis(p.x, root_min.x, root_max.x, MAX_DEPTH); + const u32 y = quantize_axis(p.y, root_min.y, root_max.y, MAX_DEPTH); + const u32 z = quantize_axis(p.z, root_min.z, root_max.z, MAX_DEPTH); return libmorton::morton3D_64_encode(x, y, z); } INLINE inline auto octree::jitter_pos(Vector3 p, - const uint32_t seed, + const u32 seed, const Vector3& root_min, const Vector3& root_max, const float root_extent) -> Vector3 @@ -173,7 +171,7 @@ INLINE inline auto octree::jitter_pos(Vector3 p, // Use a hash to calculate a deterministic jitter: The same position should always get the same jitter. // We want this to get stable physics, particles at the same position shouldn't get different jitters // across frames... - uint32_t h = (seed ^ 61u) ^ (seed >> 16); + u32 h = (seed ^ 61u) ^ (seed >> 16); h *= 9u; h = h ^ (h >> 4); h *= 0x27d4eb2du; @@ -195,14 +193,14 @@ INLINE inline auto octree::jitter_pos(Vector3 p, return p; } -INLINE inline auto octree::path_to_ancestor(const uint64_t leaf_code, const int leaf_depth, const int depth) -> uint64_t +INLINE inline auto octree::path_to_ancestor(const u64 leaf_code, const int leaf_depth, const int depth) -> u64 { // keep top 3*depth bits; drop the rest const int drop = 3 * (leaf_depth - depth); return (drop > 0) ? (leaf_code >> drop) : leaf_code; } -INLINE inline auto octree::octant_at_level(const uint64_t leaf_code, const int level, const int leaf_depth) -> int +INLINE inline auto octree::octant_at_level(const u64 leaf_code, const int level, const int leaf_depth) -> int { // level 1 => child of root => topmost 3 bits const int shift = 3 * (leaf_depth - level); diff --git a/include/orbit_camera.hpp b/include/orbit_camera.hpp index 9492d72..4fabc2c 100644 --- a/include/orbit_camera.hpp +++ b/include/orbit_camera.hpp @@ -24,8 +24,7 @@ public: auto pan(Vector2 last_mouse, Vector2 mouse) -> void; - auto update(const Vector3& current_target, const Vector3& mass_center, bool lock, - bool mass_center_lock) -> void; + auto update(const Vector3& current_target, const Vector3& mass_center, bool lock, bool mass_center_lock) -> void; }; #endif \ No newline at end of file diff --git a/include/puzzle.hpp b/include/puzzle.hpp index 3200c63..f066e35 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -4,6 +4,7 @@ #include "config.hpp" #include "util.hpp" #include "bits.hpp" +#include "cpu_spring_system.hpp" #include #include @@ -12,6 +13,7 @@ #include #include #include +#include // #define RUNTIME_CHECKS @@ -35,14 +37,14 @@ struct puzzle_hasher; * Store board size + restricted: +1 Byte * Store target position: +1 Byte * - * => Limit to 15 blocks max and use option 3. (4x uint64_t) + * => Limit to 15 blocks max and use option 3. (4x u64) * */ class puzzle { public: /* - * A block is represented as uint16_t. + * A block is represented as u16. * It stores its position, width, height and if it's the target block or immovable. */ class block @@ -50,20 +52,20 @@ public: friend class puzzle; private: - static constexpr uint16_t INVALID = 0x8000; + static constexpr u16 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; + static constexpr u8 IMMOVABLE_S = 0; + static constexpr u8 IMMOVABLE_E = 0; + static constexpr u8 TARGET_S = 1; + static constexpr u8 TARGET_E = 1; + static constexpr u8 WIDTH_S = 2; + static constexpr u8 WIDTH_E = 4; + static constexpr u8 HEIGHT_S = 5; + static constexpr u8 HEIGHT_E = 7; + static constexpr u8 X_S = 8; + static constexpr u8 X_E = 10; + static constexpr u8 Y_S = 11; + static constexpr u8 Y_E = 13; /* * Memory layout: @@ -80,16 +82,16 @@ public: * 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. + * since otherwise u16{0} would be a valid block. This also makes empty blocks sorted last. */ - uint16_t repr; + u16 repr; public: // Produces an invalid block, for usage with std::array block() : repr(INVALID) {} - explicit block(const uint16_t _repr) + explicit block(const u16 _repr) : repr(_repr) {} block(const int x, const int y, const int w, const int h, const bool t = false, const bool i = false) @@ -135,26 +137,21 @@ public: private: // Repr setters - [[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; - [[nodiscard]] inline auto set_x(uint8_t x) const -> block; - [[nodiscard]] inline auto set_y(uint8_t y) const -> block; - [[nodiscard]] inline auto set_width(uint8_t width) const -> block; - [[nodiscard]] inline auto set_height(uint8_t height) const -> block; + [[nodiscard]] static auto create_repr(u8 x, u8 y, u8 w, u8 h, bool t = false, bool i = false) -> u16; + [[nodiscard]] inline auto set_x(u8 x) const -> block; + [[nodiscard]] inline auto set_y(u8 y) const -> block; + [[nodiscard]] inline auto set_width(u8 width) const -> block; + [[nodiscard]] inline auto set_height(u8 height) const -> block; [[nodiscard]] inline auto set_target(bool target) const -> block; [[nodiscard]] inline auto set_immovable(bool immovable) const -> block; public: // Repr getters - [[nodiscard]] auto unpack_repr() const -> std::tuple; - [[nodiscard]] inline auto get_x() const -> uint8_t; - [[nodiscard]] inline auto get_y() const -> uint8_t; - [[nodiscard]] inline auto get_width() const -> uint8_t; - [[nodiscard]] inline auto get_height() const -> uint8_t; + [[nodiscard]] auto unpack_repr() const -> std::tuple; + [[nodiscard]] inline auto get_x() const -> u8; + [[nodiscard]] inline auto get_y() const -> u8; + [[nodiscard]] inline auto get_width() const -> u8; + [[nodiscard]] inline auto get_height() const -> u8; [[nodiscard]] inline auto get_target() const -> bool; [[nodiscard]] inline auto get_immovable() const -> bool; @@ -162,33 +159,45 @@ public: [[nodiscard]] auto hash() const -> size_t; [[nodiscard]] auto position_independent_hash() const -> size_t; [[nodiscard]] auto valid() const -> bool; - [[nodiscard]] auto principal_dirs() const -> uint8_t; + [[nodiscard]] auto principal_dirs() const -> u8; [[nodiscard]] auto covers(int _x, int _y) const -> bool; [[nodiscard]] auto collides(block b) const -> bool; }; public: - 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; + using blockset = boost::unordered_flat_set; + template + using blockmap = boost::unordered_flat_map; + + using blockset2 = boost::unordered_flat_set; + template + using blockmap2 = boost::unordered_flat_map; + + using puzzleset = boost::unordered_flat_set; + template + using puzzlemap = boost::unordered_flat_map; + + static constexpr u8 MAX_BLOCKS = 15; + static constexpr u8 MIN_WIDTH = 3; + static constexpr u8 MIN_HEIGHT = 3; + static constexpr u8 MAX_WIDTH = 8; + static constexpr u8 MAX_HEIGHT = 8; private: - static constexpr uint16_t INVALID = 0x8000; + static constexpr u16 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; + static constexpr u8 RESTRICTED_S = 0; + static constexpr u8 RESTRICTED_E = 0; + static constexpr u8 GOAL_X_S = 1; + static constexpr u8 GOAL_X_E = 3; + static constexpr u8 GOAL_Y_S = 4; + static constexpr u8 GOAL_Y_E = 6; + static constexpr u8 WIDTH_S = 7; + static constexpr u8 WIDTH_E = 9; + static constexpr u8 HEIGHT_S = 10; + static constexpr u8 HEIGHT_E = 12; + static constexpr u8 GOAL_S = 13; + static constexpr u8 GOAL_E = 13; struct repr_cooked { @@ -203,11 +212,11 @@ private: * 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; + u16 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; + std::array blocks; // repr_cooked() = delete; // repr_cooked(const repr_cooked& copy) = delete; @@ -226,7 +235,7 @@ private: repr_cooked cooked; // For 15 blocks, we have sizeof(meta) + blocks.size() * sizeof(block) = 2 + 15 * 2 = 32 Bytes - std::array raw; + std::array raw; }; repr_u repr; @@ -236,21 +245,21 @@ public: puzzle() : repr(repr_cooked{INVALID, invalid_blocks()}) {} - explicit puzzle(const uint16_t meta) + explicit puzzle(const u16 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) + 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) + puzzle(const u64 byte_0, const u64 byte_1, const u64 byte_2, const u64 byte_3) : repr(create_repr(byte_0, byte_1, byte_2, byte_3)) {} - puzzle(const uint16_t meta, const std::array& blocks) + // NOTE: This constructor does not sort the blocks + puzzle(const u16 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) + puzzle(const u8 w, const u8 h, const u8 tx, const u8 ty, const bool r, const bool g) : repr(create_repr(w, h, tx, ty, r, g, invalid_blocks())) { if (w < MIN_WIDTH || w > MAX_WIDTH || h < MIN_HEIGHT || h > MAX_HEIGHT) { @@ -261,13 +270,13 @@ public: } } - puzzle(const uint8_t w, - const uint8_t h, - const uint8_t tx, - const uint8_t ty, + puzzle(const u8 w, + const u8 h, + const u8 tx, + const u8 ty, const bool r, const bool g, - const std::array& b) + const std::array& b) : repr(create_repr(w, h, tx, ty, r, g, b)) { if (w < MIN_WIDTH || w > MAX_WIDTH || h < MIN_HEIGHT || h > MAX_HEIGHT) { @@ -278,7 +287,7 @@ public: } } - puzzle(const uint8_t w, const uint8_t h) + puzzle(const u8 w, const u8 h) : repr(create_repr(w, h, 0, 0, false, false, invalid_blocks())) { if (w < MIN_WIDTH || w > MAX_WIDTH || h < MIN_HEIGHT || h > MAX_HEIGHT) { @@ -333,25 +342,24 @@ public: auto repr_view() const { - return std::span(repr.cooked.blocks.data(), block_count()); + 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); - }); + return std::span(repr.cooked.blocks.data(), block_count()) | std::views::transform([](const u16 val) + { + return block(val); + }); } template static auto hash_combine(std::size_t& seed, const T& v, const Rest&... rest) -> void; private: - [[nodiscard]] static constexpr auto invalid_blocks() -> std::array + [[nodiscard]] static constexpr auto invalid_blocks() -> std::array { - std::array blocks; + std::array blocks; for (size_t i = 0; i < MAX_BLOCKS; ++i) { blocks[i] = block::INVALID; } @@ -359,37 +367,33 @@ private: } // Repr setters - [[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, + [[nodiscard]] static auto create_meta(const std::tuple& meta) -> u16; + [[nodiscard]] static auto create_repr(u8 w, + u8 h, + u8 tx, + u8 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; + const std::array& b) -> repr_cooked; + [[nodiscard]] static auto create_repr(u64 byte_0, u64 byte_1, u64 byte_2, u64 byte_3) -> repr_cooked; [[nodiscard]] static auto create_repr(const std::string& string_repr) -> repr_cooked; [[nodiscard]] inline auto set_restricted(bool restricted) const -> puzzle; - [[nodiscard]] inline auto set_width(uint8_t width) const -> puzzle; - [[nodiscard]] inline auto set_height(uint8_t height) const -> puzzle; + [[nodiscard]] inline auto set_width(u8 width) const -> puzzle; + [[nodiscard]] inline auto set_height(u8 height) const -> puzzle; [[nodiscard]] inline auto set_goal(bool goal) const -> puzzle; - [[nodiscard]] inline auto set_goal_x(uint8_t target_x) const -> puzzle; - [[nodiscard]] inline auto set_goal_y(uint8_t target_y) const -> puzzle; - [[nodiscard]] auto set_blocks(std::array blocks) const -> puzzle; + [[nodiscard]] inline auto set_goal_x(u8 target_x) const -> puzzle; + [[nodiscard]] inline auto set_goal_y(u8 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 unpack_meta() const -> std::tuple; [[nodiscard]] inline auto get_restricted() const -> bool; - [[nodiscard]] inline auto get_width() const -> uint8_t; - [[nodiscard]] inline auto get_height() const -> uint8_t; + [[nodiscard]] inline auto get_width() const -> u8; + [[nodiscard]] inline auto get_height() const -> u8; [[nodiscard]] inline auto get_goal() const -> bool; - [[nodiscard]] inline auto get_goal_x() const -> uint8_t; - [[nodiscard]] inline auto get_goal_y() const -> uint8_t; + [[nodiscard]] inline auto get_goal_x() const -> u8; + [[nodiscard]] inline auto get_goal_y() const -> u8; // Util [[nodiscard]] auto hash() const -> size_t; @@ -397,118 +401,185 @@ public: [[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; - [[nodiscard]] auto block_count() const -> uint8_t; + [[nodiscard]] auto block_count() const -> u8; [[nodiscard]] auto goal_reached() const -> bool; - [[nodiscard]] auto try_get_block(uint8_t x, uint8_t y) const -> std::optional; + [[nodiscard]] auto try_get_block(u8 x, u8 y) const -> std::optional; [[nodiscard]] auto try_get_target_block() const -> std::optional; - [[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(u8 x, u8 y, u8 _w, u8 _h) const -> bool; + [[nodiscard]] auto covers(u8 x, u8 y) const -> bool; [[nodiscard]] auto covers(block b) const -> bool; // Editing [[nodiscard]] auto toggle_restricted() const -> puzzle; - [[nodiscard]] auto try_set_goal(uint8_t x, uint8_t y) const -> std::optional; + [[nodiscard]] auto try_set_goal(u8 x, u8 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(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; + [[nodiscard]] auto try_remove_block(u8 x, u8 y) const -> std::optional; + [[nodiscard]] auto try_toggle_target(u8 x, u8 y) const -> std::optional; + [[nodiscard]] auto try_toggle_wall(u8 x, u8 y) const -> std::optional; // Bitmap - [[nodiscard]] auto blocks_bitmap() const -> uint64_t; - [[nodiscard]] auto blocks_bitmap_h() const -> uint64_t; - [[nodiscard]] auto blocks_bitmap_v() const -> uint64_t; - static INLINE inline auto bitmap_clear_bit(uint64_t& bitmap, uint8_t w, uint8_t x, uint8_t y) -> void; - static INLINE inline auto bitmap_set_bit(uint64_t& bitmap, uint8_t w, uint8_t x, uint8_t y) -> void; - [[nodiscard]] static INLINE inline auto bitmap_get_bit(uint64_t bitmap, uint8_t w, uint8_t x, uint8_t y) -> bool; - INLINE inline auto bitmap_clear_block(uint64_t& bitmap, block b) const -> void; - INLINE inline auto bitmap_set_block(uint64_t& bitmap, block b) const -> void; - [[nodiscard]] INLINE inline auto bitmap_is_empty(uint64_t bitmap) const -> bool; - [[nodiscard]] INLINE inline auto bitmap_is_full(uint64_t bitmap) const -> bool; - [[nodiscard]] INLINE inline auto bitmap_check_collision(uint64_t bitmap, block b) const -> bool; - [[nodiscard]] INLINE inline auto bitmap_check_collision(uint64_t bitmap, block b, dir dir) const -> bool; - [[nodiscard]] auto bitmap_find_first_empty(uint64_t bitmap, int& x, int& y) const -> bool; + [[nodiscard]] auto blocks_bitmap() const -> u64; + [[nodiscard]] auto blocks_bitmap_h() const -> u64; + [[nodiscard]] auto blocks_bitmap_v() const -> u64; + static INLINE inline auto bitmap_clear_bit(u64& bitmap, u8 w, u8 x, u8 y) -> void; + static INLINE inline auto bitmap_set_bit(u64& bitmap, u8 w, u8 x, u8 y) -> void; + [[nodiscard]] static INLINE inline auto bitmap_get_bit(u64 bitmap, u8 w, u8 x, u8 y) -> bool; + INLINE inline auto bitmap_clear_block(u64& bitmap, block b) const -> void; + INLINE inline auto bitmap_set_block(u64& bitmap, block b) const -> void; + [[nodiscard]] INLINE inline auto bitmap_is_empty(u64 bitmap) const -> bool; + [[nodiscard]] INLINE inline auto bitmap_is_full(u64 bitmap) const -> bool; + [[nodiscard]] INLINE inline auto bitmap_check_collision(u64 bitmap, block b) const -> bool; + [[nodiscard]] INLINE inline auto bitmap_check_collision(u64 bitmap, block b, dir dir) const -> bool; + [[nodiscard]] INLINE inline auto bitmap_find_first_empty(u64 bitmap, int& x, int& y) const -> bool; // Playing - [[nodiscard]] auto try_move_block_at(uint8_t x, uint8_t y, dir dir) const -> std::optional; + [[nodiscard]] auto try_move_block_at(u8 x, u8 y, dir dir) const -> std::optional; // Statespace - [[nodiscard]] INLINE inline auto try_move_block_at_fast(uint64_t bitmap, - uint8_t block_idx, + + [[nodiscard]] INLINE inline auto try_move_block_at_fast(u64 bitmap, + u8 block_idx, dir dir, bool check_collision = true) const -> std::optional; - [[nodiscard]] static auto sorted_replace(std::array blocks, - uint8_t idx, - uint16_t new_val) -> std::array; - + [[nodiscard]] INLINE static inline auto sorted_replace(std::array blocks, + u8 idx, + u16 new_val) -> std::array; template // ReSharper disable once CppRedundantInlineSpecifier - INLINE inline 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 dir d : {nor, eas, sou, wes}) { - if (dirs & d) { - if (auto moved = try_move_block_at_fast(bitmap, idx, d)) { - callback(*moved); - } - } - } - } - } + INLINE inline auto for_each_adjacent(F&& callback) const -> void; - [[nodiscard]] auto explore_state_space() const - -> std::pair, std::vector>>; + [[nodiscard]] auto explore_state_space() const -> std::pair, std::vector>; + + // Puzzle space // Determines to which cluster a puzzle belongs. Clusters are identified by the // state with the numerically smallest binary representation. - [[nodiscard]] auto get_cluster_id_and_solution() const -> std::pair; + [[nodiscard]] auto get_cluster_id_and_solution() const -> std::tuple, bool, int>; - static auto generate_block_sequences( - const boost::unordered_flat_set& permitted_blocks, - block target_block, - size_t max_blocks, - std::vector& current_sequence, - int current_area, - int board_area, - const std::function&)>& callback) -> void; + static auto generate_block_sets(const puzzle& p, + const blockset2& permitted_blocks, + block target_block, + u8 max_blocks) -> std::vector>; - static auto place_block_sequence(const puzzle& p, - const uint64_t& bitmap, - const std::tuple& p_repr, - const std::vector& sequence, - block target_block, - const std::tuple& target_block_pos_range, - bool has_target, - size_t index, - const std::function& callback) -> void; + static auto generate_initial_puzzles(const puzzle& p, + block target_block, + const std::tuple& target_block_pos_range) -> std::vector< + puzzle>; - [[nodiscard]] auto explore_puzzle_space( - const boost::unordered_flat_set& permitted_blocks, - block target_block, - const std::tuple& target_block_pos_range, - size_t max_blocks, - std::optional* const> thread_pool = std::nullopt) const -> boost::unordered_flat_set< - puzzle, puzzle_hasher>; + template + static auto place_block_set(const puzzle& p, const blockmap2& set, u64 bitmap, u8 block_index, F&& callback) -> void; + + [[nodiscard]] auto explore_puzzle_space(const blockset2& permitted_blocks, + block target_block, + const std::tuple& target_block_pos_range, + size_t max_blocks, + int min_moves, + threadpool thread_pool = std::nullopt) const -> puzzleset; }; +// Type aliases +using block = puzzle::block; +using blockset = puzzle::blockset; +template +using blockmap = puzzle::blockmap; +using blockset2 = puzzle::blockset2; +template +using blockmap2 = puzzle::blockmap2; +using puzzleset = puzzle::puzzleset; +template +using puzzlemap = puzzle::puzzlemap; + +// Hash functions for sets and maps. +// Declared after puzzle class to use puzzle::hash_combine +#ifndef REGION_HASHERS + +struct block_hasher +{ + auto operator()(const block& b) const noexcept -> size_t + { + return b.hash(); + } +}; + +struct block_hasher2 +{ + auto operator()(const block& b) const noexcept -> size_t + { + return b.position_independent_hash(); + } +}; + +struct block_equal2 +{ + auto operator()(const block& a, const block& b) const noexcept -> bool + { + const auto [ax, ay, aw, ah, at, ai] = a.unpack_repr(); + const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); + + return aw == bw && ah == bh && at == bt && ai == bi; + } +}; + +struct puzzle_hasher +{ + auto operator()(const puzzle& s) const noexcept -> size_t + { + return s.hash(); + } +}; + +struct link_hasher +{ + auto operator()(const std::pair& s) const noexcept -> size_t + { + 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; + } +}; + +struct link_equal +{ + 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); + } +}; + +template +// ReSharper disable once CppRedundantInlineSpecifier +INLINE inline auto puzzle::hash_combine(size_t& seed, const T& v, const Rest&... rest) -> void +{ + auto hasher = [](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 ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + (hash_combine(seed, rest), ...); +} + +#endif + // Inline functions definitions #ifndef REGION_INLINE_DEFS -inline auto puzzle::block::set_x(const uint8_t x) const -> block +inline auto block::set_x(const u8 x) const -> block { #ifdef RUNTIME_CHECKS if (x > 7) { @@ -521,7 +592,7 @@ inline auto puzzle::block::set_x(const uint8_t x) const -> block return b; } -inline auto puzzle::block::set_y(const uint8_t y) const -> block +inline auto block::set_y(const u8 y) const -> block { #ifdef RUNTIME_CHECKS if (y > 7) { @@ -534,7 +605,7 @@ inline auto puzzle::block::set_y(const uint8_t y) const -> block return b; } -inline auto puzzle::block::set_width(const uint8_t width) const -> block +inline auto block::set_width(const u8 width) const -> block { #ifdef RUNTIME_CHECKS if (width - 1 > 7) { @@ -547,7 +618,7 @@ inline auto puzzle::block::set_width(const uint8_t width) const -> block return b; } -inline auto puzzle::block::set_height(const uint8_t height) const -> block +inline auto block::set_height(const u8 height) const -> block { #ifdef RUNTIME_CHECKS if (height - 1 > 7) { @@ -560,58 +631,58 @@ inline auto puzzle::block::set_height(const uint8_t height) const -> block return b; } -inline auto puzzle::block::set_target(const bool target) const -> block +inline auto block::set_target(const bool target) const -> block { block b = *this; set_bits(b.repr, TARGET_S, TARGET_E, target); return b; } -inline auto puzzle::block::set_immovable(const bool immovable) const -> block +inline auto block::set_immovable(const bool immovable) const -> block { block b = *this; set_bits(b.repr, IMMOVABLE_S, IMMOVABLE_E, immovable); return b; } -inline auto puzzle::block::get_x() const -> uint8_t +inline auto block::get_x() const -> u8 { return get_bits(repr, X_S, X_E); } -inline auto puzzle::block::get_y() const -> uint8_t +inline auto block::get_y() const -> u8 { return get_bits(repr, Y_S, Y_E); } -inline auto puzzle::block::get_width() const -> uint8_t +inline auto block::get_width() const -> u8 { return get_bits(repr, WIDTH_S, WIDTH_E) + 1u; } -inline auto puzzle::block::get_height() const -> uint8_t +inline auto block::get_height() const -> u8 { return get_bits(repr, HEIGHT_S, HEIGHT_E) + 1u; } -inline auto puzzle::block::get_target() const -> bool +inline auto block::get_target() const -> bool { return get_bits(repr, TARGET_S, TARGET_E); } -inline auto puzzle::block::get_immovable() const -> bool +inline auto block::get_immovable() const -> bool { return get_bits(repr, IMMOVABLE_S, IMMOVABLE_E); } inline auto puzzle::set_restricted(const bool restricted) const -> puzzle { - uint16_t meta = repr.cooked.meta; + u16 meta = repr.cooked.meta; set_bits(meta, RESTRICTED_S, RESTRICTED_E, restricted); return puzzle(meta, repr.cooked.blocks); } -inline auto puzzle::set_width(const uint8_t width) const -> puzzle +inline auto puzzle::set_width(const u8 width) const -> puzzle { #ifdef RUNTIME_CHECKS if (width - 1 > MAX_WIDTH) { @@ -619,12 +690,12 @@ inline auto puzzle::set_width(const uint8_t width) const -> puzzle } #endif - uint16_t meta = repr.cooked.meta; + u16 meta = repr.cooked.meta; set_bits(meta, WIDTH_S, WIDTH_E, width - 1u); return puzzle(meta, repr.cooked.blocks); } -inline auto puzzle::set_height(const uint8_t height) const -> puzzle +inline auto puzzle::set_height(const u8 height) const -> puzzle { #ifdef RUNTIME_CHECKS if (height - 1 > MAX_HEIGHT) { @@ -632,19 +703,19 @@ inline auto puzzle::set_height(const uint8_t height) const -> puzzle } #endif - uint16_t meta = repr.cooked.meta; + u16 meta = repr.cooked.meta; set_bits(meta, HEIGHT_S, HEIGHT_E, height - 1u); return puzzle(meta, repr.cooked.blocks); } inline auto puzzle::set_goal(const bool goal) const -> puzzle { - uint16_t meta = repr.cooked.meta; + u16 meta = repr.cooked.meta; set_bits(meta, GOAL_S, GOAL_E, goal); return puzzle(meta, repr.cooked.blocks); } -inline auto puzzle::set_goal_x(const uint8_t target_x) const -> puzzle +inline auto puzzle::set_goal_x(const u8 target_x) const -> puzzle { #ifdef RUNTIME_CHECKS if (target_x >= MAX_WIDTH) { @@ -652,12 +723,12 @@ inline auto puzzle::set_goal_x(const uint8_t target_x) const -> puzzle } #endif - uint16_t meta = repr.cooked.meta; + u16 meta = repr.cooked.meta; set_bits(meta, GOAL_X_S, GOAL_X_E, target_x); return puzzle(meta, repr.cooked.blocks); } -inline auto puzzle::set_goal_y(const uint8_t target_y) const -> puzzle +inline auto puzzle::set_goal_y(const u8 target_y) const -> puzzle { #ifdef RUNTIME_CHECKS if (target_y >= MAX_HEIGHT) { @@ -665,7 +736,7 @@ inline auto puzzle::set_goal_y(const uint8_t target_y) const -> puzzle } #endif - uint16_t meta = repr.cooked.meta; + u16 meta = repr.cooked.meta; set_bits(meta, GOAL_Y_S, GOAL_Y_E, target_y); return puzzle(meta, repr.cooked.blocks); } @@ -675,12 +746,12 @@ inline auto puzzle::get_restricted() const -> bool return get_bits(repr.cooked.meta, RESTRICTED_S, RESTRICTED_E); } -inline auto puzzle::get_width() const -> uint8_t +inline auto puzzle::get_width() const -> u8 { return get_bits(repr.cooked.meta, WIDTH_S, WIDTH_E) + 1u; } -inline auto puzzle::get_height() const -> uint8_t +inline auto puzzle::get_height() const -> u8 { return get_bits(repr.cooked.meta, HEIGHT_S, HEIGHT_E) + 1u; } @@ -690,38 +761,35 @@ inline auto puzzle::get_goal() const -> bool return get_bits(repr.cooked.meta, GOAL_S, GOAL_E); } -inline auto puzzle::get_goal_x() const -> uint8_t +inline auto puzzle::get_goal_x() const -> u8 { return get_bits(repr.cooked.meta, GOAL_X_S, GOAL_X_E); } -inline auto puzzle::get_goal_y() const -> uint8_t +inline auto puzzle::get_goal_y() const -> u8 { return get_bits(repr.cooked.meta, GOAL_Y_S, GOAL_Y_E); } -INLINE inline auto puzzle::bitmap_clear_bit(uint64_t& bitmap, const uint8_t w, const uint8_t x, const uint8_t y) -> void +INLINE inline auto puzzle::bitmap_clear_bit(u64& bitmap, const u8 w, const u8 x, const u8 y) -> void { set_bits(bitmap, y * w + x, y * w + x, 0u); } -INLINE inline auto puzzle::bitmap_set_bit(uint64_t& bitmap, const uint8_t w, const uint8_t x, const uint8_t y) -> void +INLINE inline auto puzzle::bitmap_set_bit(u64& bitmap, const u8 w, const u8 x, const u8 y) -> void { set_bits(bitmap, y * w + x, y * w + x, 1u); } -INLINE inline auto puzzle::bitmap_get_bit(const uint64_t bitmap, - const uint8_t w, - const uint8_t x, - const uint8_t y) -> bool +INLINE inline auto puzzle::bitmap_get_bit(const u64 bitmap, const u8 w, const u8 x, const u8 y) -> bool { return get_bits(bitmap, y * w + x, y * w + x); } -INLINE inline auto puzzle::bitmap_clear_block(uint64_t& bitmap, const block b) const -> void +INLINE inline auto puzzle::bitmap_clear_block(u64& bitmap, const block b) const -> void { const auto [x, y, w, h, t, i] = b.unpack_repr(); - const uint8_t width = get_width(); + const u8 width = get_width(); for (int dy = 0; dy < h; ++dy) { for (int dx = 0; dx < w; ++dx) { @@ -730,10 +798,10 @@ INLINE inline auto puzzle::bitmap_clear_block(uint64_t& bitmap, const block b) c } } -INLINE inline auto puzzle::bitmap_set_block(uint64_t& bitmap, const block b) const -> void +INLINE inline auto puzzle::bitmap_set_block(u64& bitmap, const block b) const -> void { const auto [x, y, w, h, t, i] = b.unpack_repr(); - const uint8_t width = get_width(); + const u8 width = get_width(); for (int dy = 0; dy < h; ++dy) { for (int dx = 0; dx < w; ++dx) { @@ -742,22 +810,22 @@ INLINE inline auto puzzle::bitmap_set_block(uint64_t& bitmap, const block b) con } } -INLINE inline auto puzzle::bitmap_is_empty(const uint64_t bitmap) const -> bool +INLINE inline auto puzzle::bitmap_is_empty(const u64 bitmap) const -> bool { - const uint8_t shift = 64 - get_width() * get_height(); + const u8 shift = 64 - get_width() * get_height(); return bitmap << shift == 0; } -INLINE inline auto puzzle::bitmap_is_full(const uint64_t bitmap) const -> bool +INLINE inline auto puzzle::bitmap_is_full(const u64 bitmap) const -> bool { - const uint8_t shift = 64 - get_width() * get_height(); - return ((bitmap << shift) >> shift) == ((static_cast(-1) << shift) >> shift); + const u8 shift = 64 - get_width() * get_height(); + return ((bitmap << shift) >> shift) == ((static_cast(-1) << shift) >> shift); } -INLINE inline auto puzzle::bitmap_check_collision(const uint64_t bitmap, const block b) const -> bool +INLINE inline auto puzzle::bitmap_check_collision(const u64 bitmap, const block b) const -> bool { const auto [x, y, w, h, t, i] = b.unpack_repr(); - const uint8_t width = get_width(); + const u8 width = get_width(); for (int dy = 0; dy < h; ++dy) { for (int dx = 0; dx < w; ++dx) { @@ -770,12 +838,10 @@ INLINE inline auto puzzle::bitmap_check_collision(const uint64_t bitmap, const b return false; } -INLINE inline auto puzzle::bitmap_check_collision(const uint64_t bitmap, - const block b, - const dir dir) const -> bool +INLINE inline auto puzzle::bitmap_check_collision(const u64 bitmap, const block b, const dir dir) const -> bool { const auto [x, y, w, h, t, i] = b.unpack_repr(); - const uint8_t width = get_width(); + const u8 width = get_width(); switch (dir) { case nor: // Check the row above: (x...x+w-1, y-1) @@ -810,8 +876,36 @@ INLINE inline auto puzzle::bitmap_check_collision(const uint64_t bitmap, return false; } -INLINE inline auto puzzle::try_move_block_at_fast(uint64_t bitmap, - const uint8_t block_idx, +INLINE inline auto puzzle::bitmap_find_first_empty(const u64 bitmap, int& x, int& y) const -> bool +{ + x = 0; + y = 0; + + // Bitmap is empty of first slot is empty + if (bitmap_is_empty(bitmap) || !(bitmap & 1u)) { + return true; + } + + // Bitmap is full + if (bitmap_is_full(bitmap)) { + return false; + } + + // Find the next more significant empty bit (we know the first slot is full) + int ls_set = 0; + bool next_set = true; + while (next_set && ls_set < get_width() * get_height() - 1) { + next_set = bitmap & (1ul << (ls_set + 1)); + ++ls_set; + } + + x = ls_set % get_width(); + y = ls_set / get_width(); + return true; +} + +INLINE inline auto puzzle::try_move_block_at_fast(u64 bitmap, + const u8 block_idx, const dir dir, const bool check_collision) const -> std::optional { @@ -863,98 +957,125 @@ INLINE inline auto puzzle::try_move_block_at_fast(uint64_t bitmap, } // Replace block - const std::array blocks = sorted_replace(repr.cooked.blocks, - block_idx, - block::create_repr( - _target_x, - _target_y, - bw, - bh, - bt)); + 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); } -#endif - -// Hash functions for sets and maps. -// Declared after puzzle class to use puzzle::hash_combine -#ifndef REGION_HASHERS - -struct block_hasher +INLINE inline auto puzzle::sorted_replace(std::array blocks, + const u8 idx, + const u16 new_val) -> std::array { - auto operator()(const puzzle::block& b) const noexcept -> size_t - { - return b.hash(); + // Remove old entry + for (u8 i = idx; i < MAX_BLOCKS - 1; ++i) { + blocks[i] = blocks[i + 1]; } -}; + blocks[MAX_BLOCKS - 1] = block::INVALID; -struct block_hasher2 -{ - auto operator()(const puzzle::block& b) const noexcept -> size_t - { - return b.position_independent_hash(); + // Find insertion point for new_val + u8 insert_at = 0; + while (insert_at < MAX_BLOCKS && blocks[insert_at] < new_val) { + ++insert_at; } -}; -struct block_equal2 -{ - auto operator()(const puzzle::block& a, const puzzle::block& b) const noexcept -> bool - { - const auto [ax, ay, aw, ah, at, ai] = a.unpack_repr(); - const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); - - return aw == bw && ah == bh && at == bt && ai == bi; + // Shift right and insert + for (u8 i = MAX_BLOCKS - 1; i > insert_at; --i) { + blocks[i] = blocks[i - 1]; } -}; + blocks[insert_at] = new_val; -struct puzzle_hasher -{ - auto operator()(const puzzle& s) const noexcept -> size_t - { - return s.hash(); - } -}; + return blocks; +} -struct link_hasher +template +// ReSharper disable once CppRedundantInlineSpecifier +INLINE inline auto puzzle::for_each_adjacent(F&& callback) const -> void { - auto operator()(const std::pair& s) const noexcept -> size_t - { - 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); + const u64 bitmap = blocks_bitmap(); + const bool r = get_restricted(); + for (u8 idx = 0; idx < MAX_BLOCKS; idx++) { + const block b = block(repr.cooked.blocks[idx]); + if (!b.valid()) { + break; } - return h; - } -}; - -struct link_equal -{ - 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); - } -}; - -template -auto puzzle::hash_combine(size_t& seed, const T& v, const Rest&... rest) -> void -{ - auto hasher = [](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); + if (b.get_immovable()) { + continue; } - }; + const int dirs = r ? b.principal_dirs() : nor | eas | sou | wes; + for (const dir d : {nor, eas, sou, wes}) { + if (dirs & d) { + if (auto moved = try_move_block_at_fast(bitmap, idx, d)) { + callback(*moved); + } + } + } + } +} - seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); - (hash_combine(seed, rest), ...); +// TODO: Work on a mutable set + bitmap. Do change, call subtree, then undo change after to reduce copying +template +auto puzzle::place_block_set(const puzzle& p, + const blockmap2& set, + const u64 bitmap, + const u8 block_index, + F&& callback) -> void +{ + // All blocks placed. We don't need to check for max_blocks because block set generation does + bool blocks_remain = false; + for (const u8 count : set | std::views::values) { + if (count > 0) { + blocks_remain = true; + break; + } + } + if (!blocks_remain) { + callback(p); + return; + } + + int x, y; + if (!p.bitmap_find_first_empty(bitmap, x, y)) { + // No space remaining + callback(p); + return; + } + + for (const auto& [_b, count] : set) { + // Already picked all of those blocks + if (count == 0) { + continue; + } + + // Take block from the inventory + blockmap2 next_set = set; + --next_set[_b]; + + block b = _b; + b = b.set_x(static_cast(x)); + b = b.set_y(static_cast(y)); + + // Place the next block and call the resulting subtree, then remove the block and continue here + if (!p.bitmap_check_collision(bitmap, b) && p.covers(b)) { + // We don't need to sort during puzzle generation, only after + std::array blocks = p.repr.cooked.blocks; + blocks[block_index] = b.repr; + const puzzle next_p = puzzle(p.repr.cooked.meta, blocks); + + u64 next_bm = bitmap; + next_p.bitmap_set_block(next_bm, b); + // print_bitmap(next_bm, p.get_width(), p.get_height(), "Next Block"); + place_block_set(next_p, next_set, next_bm, block_index + 1, callback); + } + } + + // Create an empty cell and call the resulting subtree (without advancing the block index) + u64 next_bm = bitmap; + bitmap_set_bit(next_bm, p.get_width(), x, y); + // print_bitmap(next_bm, p.get_width(), p.get_height(), "Empty Cell"); + place_block_set(p, set, next_bm, block_index, callback); } #endif diff --git a/include/renderer.hpp b/include/renderer.hpp index 3d8d741..9cc51ce 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -92,10 +92,7 @@ public: rlDisableVertexArray(); } - renderer(const renderer& copy) = delete; - auto operator=(const renderer& copy) -> renderer& = delete; - renderer(renderer&& move) = delete; - auto operator=(renderer&& move) -> renderer& = delete; + NO_COPY_NO_MOVE(renderer); ~renderer() { diff --git a/include/state_manager.hpp b/include/state_manager.hpp index da015b6..6d298a3 100644 --- a/include/state_manager.hpp +++ b/include/state_manager.hpp @@ -22,9 +22,9 @@ private: // 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 - boost::unordered_flat_map state_indices; // Maps states to indices - std::vector> links; // Indices are equal to mass_springs springs indices + std::vector state_pool; // Indices are equal to mass_springs mass indices + puzzlemap 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 boost::unordered_flat_set winning_indices; // Indices of all states where the board is solved @@ -48,10 +48,7 @@ public: reload_preset_file(); } - state_manager(const state_manager& copy) = delete; - auto operator=(const state_manager& copy) -> state_manager& = delete; - state_manager(state_manager&& move) = delete; - auto operator=(state_manager&& move) -> state_manager& = delete; + NO_COPY_NO_MOVE(state_manager); private: /** @@ -83,8 +80,7 @@ private: * @param states List of states to insert * @param _links List of links to insert */ - auto synced_insert_statespace(const std::vector& states, - const std::vector>& _links) -> void; + auto synced_insert_statespace(const std::vector& states, const std::vector& _links) -> void; /** * Clears all states and links (and related) from the state_manager and the physics system. @@ -130,7 +126,7 @@ public: [[nodiscard]] auto get_target_count() const -> size_t; [[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_links() const -> const std::vector&; [[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&; diff --git a/include/user_interface.hpp b/include/user_interface.hpp index 84f77a4..30909d2 100644 --- a/include/user_interface.hpp +++ b/include/user_interface.hpp @@ -22,15 +22,17 @@ class user_interface const int padding; public: - grid(const int _x, const int _y, const int _width, const int _height, const int _columns, - const int _rows, const int _padding) - : x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows), - padding(_padding) - {} + grid(const int _x, + const int _y, + const int _width, + const int _height, + const int _columns, + const int _rows, + const int _padding) + : x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows), padding(_padding) {} public: - auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows) - -> void; + auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows) -> void; auto update_bounds(int _x, int _y, int _width, int _height) -> void; auto update_bounds(int _x, int _y) -> void; @@ -38,8 +40,7 @@ class user_interface [[nodiscard]] auto bounds(int _x, int _y, int _width, int _height) const -> Rectangle; [[nodiscard]] auto square_bounds() const -> Rectangle; - [[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const - -> Rectangle; + [[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const -> Rectangle; }; struct style @@ -87,14 +88,17 @@ private: grid menu_grid = grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD); - grid board_grid = - grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, - state.get_current_state().get_width(), state.get_current_state().get_height(), BOARD_PADDING); + grid board_grid = grid(0, + MENU_HEIGHT, + GetScreenWidth() / 2, + GetScreenHeight() - MENU_HEIGHT, + 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); - grid debug_overlay_grid = - grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD); + grid debug_overlay_grid = grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD); // Windows @@ -114,10 +118,7 @@ public: init(); } - user_interface(const user_interface& copy) = delete; - auto operator=(const user_interface& copy) -> user_interface& = delete; - user_interface(user_interface&& move) = delete; - auto operator=(user_interface&& move) -> user_interface& = delete; + NO_COPY_NO_MOVE(user_interface); private: static auto init() -> void; @@ -133,32 +134,68 @@ private: [[nodiscard]] static auto popup_bounds() -> Rectangle; - auto draw_button(Rectangle bounds, const std::string& label, Color color, bool enabled = true, + auto draw_button(Rectangle bounds, + const std::string& label, + Color color, + bool enabled = true, int font_size = FONT_SIZE) const -> int; - auto draw_menu_button(int x, int y, int width, int height, const std::string& label, - Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int; + auto draw_menu_button(int x, + int y, + int width, + int height, + const std::string& label, + Color color, + bool enabled = true, + int font_size = FONT_SIZE) const -> int; - auto draw_toggle_slider(Rectangle bounds, const std::string& off_label, - const std::string& on_label, int* active, Color color, - bool enabled = true, int font_size = FONT_SIZE) const -> int; + auto draw_toggle_slider(Rectangle bounds, + const std::string& off_label, + const std::string& on_label, + int* active, + Color color, + bool enabled = true, + int font_size = FONT_SIZE) const -> int; - auto draw_menu_toggle_slider(int x, int y, int width, int height, const std::string& off_label, - const std::string& on_label, int* active, Color color, - bool enabled = true, int font_size = FONT_SIZE) const -> int; + auto draw_menu_toggle_slider(int x, + int y, + int width, + int height, + const std::string& off_label, + const std::string& on_label, + int* active, + Color color, + bool enabled = true, + int font_size = FONT_SIZE) const -> int; - auto draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max, - Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int; + auto draw_spinner(Rectangle bounds, + const std::string& label, + int* value, + int min, + int max, + Color color, + bool enabled = true, + int font_size = FONT_SIZE) const -> int; - auto draw_menu_spinner(int x, int y, int width, int height, const std::string& label, - int* value, int min, int max, Color color, bool enabled = true, + auto draw_menu_spinner(int x, + int y, + int width, + int height, + const std::string& label, + int* value, + int min, + int max, + Color color, + bool enabled = true, int font_size = FONT_SIZE) const -> int; - auto draw_label(Rectangle bounds, const std::string& text, Color color, bool enabled = true, + auto draw_label(Rectangle bounds, + const std::string& text, + Color color, + bool enabled = true, int font_size = FONT_SIZE) const -> int; - auto draw_board_block(int x, int y, int width, int height, Color color, - bool enabled = true) const -> bool; + auto draw_board_block(int x, int y, int width, int height, Color color, bool enabled = true) const -> bool; [[nodiscard]] auto window_open() const -> bool; diff --git a/include/util.hpp b/include/util.hpp index f80c00e..01c8647 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -1,6 +1,7 @@ #ifndef UTIL_HPP_ #define UTIL_HPP_ +#include #include #include @@ -8,10 +9,26 @@ #define PACKED __attribute__((packed)) #define STARTTIME const auto start = std::chrono::high_resolution_clock::now() -#define ENDTIME(msg) const auto end = std::chrono::high_resolution_clock::now(); \ - infoln("{}. Took {}ms.", msg, std::chrono::duration_cast(end - start).count()) +#define ENDTIME(msg, cast, unit) const auto end = std::chrono::high_resolution_clock::now(); \ + infoln("{}. Took {}{}.", msg, std::chrono::duration_cast(end - start).count(), unit) -// std::variant visitor +#define COMMENT if (false) + +#define NO_COPY_NO_MOVE(typename) \ + typename(const typename& copy) = delete; \ + auto operator=(const typename& copy) -> typename& = delete; \ + typename(typename&& move) = delete; \ + auto operator=(typename&& move) -> typename& = delete; + +using u8 = uint8_t; +using u16 = uint16_t; +using u32 = uint32_t; +using u64 = uint64_t; + +using i8 = int8_t; +using i16 = int16_t; +using i32 = int32_t; +using i64 = int64_t; // https://en.cppreference.com/w/cpp/utility/variant/visit template @@ -20,9 +37,21 @@ struct overloads : Ts... using Ts::operator()...; }; +inline auto binom(const int n, const int k) -> int +{ + std::vector solutions(k); + solutions[0] = n - k + 1; + + for (int i = 1; i < k; ++i) { + solutions[i] = solutions[i - 1] * (n - k + 1 + i) / (i + 1); + } + + return solutions[k - 1]; +} + // Enums -enum dir : uint8_t +enum dir : u8 { nor = 1 << 0, eas = 1 << 1, @@ -32,7 +61,7 @@ enum dir : uint8_t // Ansi -enum class ctrl : uint8_t +enum class ctrl : u8 { reset = 0, bold_bright = 1, @@ -43,7 +72,7 @@ enum class ctrl : uint8_t inverse_off = 27 }; -enum class fg : uint8_t +enum class fg : u8 { black = 30, red = 31, @@ -55,7 +84,7 @@ enum class fg : uint8_t white = 37 }; -enum class bg : uint8_t +enum class bg : u8 { black = 40, red = 41, diff --git a/src/bits.cpp b/src/bits.cpp index 3677139..48bd5d5 100644 --- a/src/bits.cpp +++ b/src/bits.cpp @@ -1,12 +1,12 @@ #include "bits.hpp" -auto print_bitmap(const uint64_t bitmap, const uint8_t w, const uint8_t h, const std::string& title) -> void { +auto print_bitmap(const u64 bitmap, const u8 w, const u8 h, const std::string& title) -> void { traceln("{}:", title); traceln("{}", std::string(2 * w - 1, '=')); - for (size_t y = 0; y < w; ++y) { + for (size_t y = 0; y < h; ++y) { std::cout << " "; - for (size_t x = 0; x < h; ++x) { - std::cout << static_cast(get_bits(bitmap, y * w + x, y * h + x)) << " "; + for (size_t x = 0; x < w; ++x) { + std::cout << static_cast(get_bits(bitmap, y * w + x, y * w + x)) << " "; } std::cout << "\n"; } diff --git a/src/cpu_layout_engine.cpp b/src/cpu_layout_engine.cpp index 4882e93..e3a6782 100644 --- a/src/cpu_layout_engine.cpp +++ b/src/cpu_layout_engine.cpp @@ -17,7 +17,7 @@ auto cpu_layout_engine::set_octree_pool_thread_name(size_t idx) -> void } #endif -auto cpu_layout_engine::physics_thread(physics_state& state, const std::optional* const> thread_pool) -> void +auto cpu_layout_engine::physics_thread(physics_state& state, const threadpool thread_pool) -> void { cpu_spring_system mass_springs; @@ -138,10 +138,11 @@ auto cpu_layout_engine::physics_thread(physics_state& state, const std::optional #else std::unique_lock lock(state.data_mtx); #endif - state.data_consumed_cnd.wait(lock, [&] - { - return state.data_consumed || !state.running.load(); - }); + state.data_consumed_cnd.wait(lock, + [&] + { + return state.data_consumed || !state.running.load(); + }); if (!state.running.load()) { // Running turned false while we were waiting for the condition break; @@ -175,7 +176,8 @@ auto cpu_layout_engine::physics_thread(physics_state& state, const std::optional state.data_ready_cnd.notify_all(); #ifdef TRACY - FrameMarkEnd("PhysicsThreadProduceLock"); FrameMarkEnd("PhysicsThread"); + FrameMarkEnd("PhysicsThreadProduceLock"); + FrameMarkEnd("PhysicsThread"); #endif } } @@ -216,8 +218,7 @@ auto cpu_layout_engine::add_spring_cmd(const size_t a, const size_t b) -> void } } -auto cpu_layout_engine::add_mass_springs_cmd(const size_t num_masses, - const std::vector>& springs) -> void +auto cpu_layout_engine::add_mass_springs_cmd(const size_t num_masses, const std::vector& springs) -> void { { #ifdef TRACY diff --git a/src/cpu_spring_system.cpp b/src/cpu_spring_system.cpp index b86b09c..4d82350 100644 --- a/src/cpu_spring_system.cpp +++ b/src/cpu_spring_system.cpp @@ -37,17 +37,18 @@ auto cpu_spring_system::add_spring(size_t a, size_t b) -> void const Vector3& mass_a = positions[a]; const Vector3& mass_b = positions[b]; - Vector3 offset{static_cast(GetRandomValue(-100, 100)), + Vector3 offset{ static_cast(GetRandomValue(-100, 100)), - static_cast(GetRandomValue(-100, 100))}; + static_cast(GetRandomValue(-100, 100)), + static_cast(GetRandomValue(-100, 100)) + }; // By spawning the masses close together, we "explode" them naturally, so they cluster faster (also looks cool) offset = Vector3Normalize(offset) * REST_LENGTH * 0.1; // If the offset moves the mass closer to the current center of mass, flip it if (!tree.empty()) { - const Vector3 mass_center_direction = - Vector3Subtract(positions[a], tree.root().mass_center); + const Vector3 mass_center_direction = Vector3Subtract(positions[a], tree.root().mass_center); const float mass_center_distance = Vector3Length(mass_center_direction); if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) { @@ -77,10 +78,10 @@ auto cpu_spring_system::clear_forces() -> void auto cpu_spring_system::calculate_spring_force(const size_t s) -> void { const spring _s = springs[s]; - const Vector3 a_pos = positions[_s.a]; - const Vector3 b_pos = positions[_s.b]; - const Vector3 a_vel = velocities[_s.a]; - const Vector3 b_vel = velocities[_s.b]; + const Vector3 a_pos = positions[_s.first]; + const Vector3 b_pos = positions[_s.second]; + const Vector3 a_vel = velocities[_s.first]; + const Vector3 b_vel = velocities[_s.second]; const Vector3 delta_pos = a_pos - b_pos; const Vector3 delta_vel = a_vel - b_vel; @@ -95,23 +96,23 @@ auto cpu_spring_system::calculate_spring_force(const size_t s) -> void const Vector3 a_force = Vector3Scale(delta_pos, -(hooke + dampening) * inv_len); const Vector3 b_force = a_force * -1.0f; - forces[_s.a] += a_force; - forces[_s.b] += b_force; + forces[_s.first] += a_force; + forces[_s.second] += b_force; } -auto cpu_spring_system::calculate_spring_forces( - const std::optional* const> thread_pool) -> void +auto cpu_spring_system::calculate_spring_forces(const threadpool thread_pool) -> void { #ifdef TRACY ZoneScoped; #endif - const auto solve_spring_force = [&](const int i) { calculate_spring_force(i); }; + const auto solve_spring_force = [&](const int i) + { + calculate_spring_force(i); + }; if (thread_pool) { - (*thread_pool) - ->submit_loop(0, springs.size(), solve_spring_force, SMALL_TASK_BLOCK_SIZE) - .wait(); + (*thread_pool)->submit_loop(0, springs.size(), solve_spring_force, SMALL_TASK_BLOCK_SIZE).wait(); } else { for (size_t i = 0; i < springs.size(); ++i) { solve_spring_force(i); @@ -119,8 +120,7 @@ auto cpu_spring_system::calculate_spring_forces( } } -auto cpu_spring_system::calculate_repulsion_forces( - const std::optional* const> thread_pool) -> void +auto cpu_spring_system::calculate_repulsion_forces(const threadpool thread_pool) -> void { #ifdef TRACY ZoneScoped; @@ -134,9 +134,7 @@ auto cpu_spring_system::calculate_repulsion_forces( // Calculate forces using Barnes-Hut if (thread_pool) { - (*thread_pool) - ->submit_loop(0, positions.size(), solve_octree, LARGE_TASK_BLOCK_SIZE) - .wait(); + (*thread_pool)->submit_loop(0, positions.size(), solve_octree, LARGE_TASK_BLOCK_SIZE).wait(); } else { for (size_t i = 0; i < positions.size(); ++i) { solve_octree(i); @@ -168,14 +166,16 @@ auto cpu_spring_system::verlet_update(const size_t m, const float dt) -> void previous_positions[m] = pos; } -auto cpu_spring_system::update(const float dt, - const std::optional* const> thread_pool) -> void +auto cpu_spring_system::update(const float dt, const threadpool thread_pool) -> void { #ifdef TRACY ZoneScoped; #endif - const auto update = [&](const int i) { verlet_update(i, dt); }; + const auto update = [&](const int i) + { + verlet_update(i, dt); + }; if (thread_pool) { (*thread_pool)->submit_loop(0, positions.size(), update, SMALL_TASK_BLOCK_SIZE).wait(); @@ -186,8 +186,7 @@ auto cpu_spring_system::update(const float dt, } } -auto cpu_spring_system::center_masses(const std::optional* const> thread_pool) - -> void +auto cpu_spring_system::center_masses(const threadpool thread_pool) -> void { Vector3 mean = Vector3Zero(); for (const Vector3& pos : positions) { @@ -195,7 +194,10 @@ auto cpu_spring_system::center_masses(const std::optional* con } mean /= static_cast(positions.size()); - const auto center_mass = [&](const int i) { positions[i] -= mean; }; + const auto center_mass = [&](const int i) + { + positions[i] -= mean; + }; if (thread_pool) { (*thread_pool)->submit_loop(0, positions.size(), center_mass, SMALL_TASK_BLOCK_SIZE).wait(); diff --git a/src/graph_distances.cpp b/src/graph_distances.cpp index 129ce3e..0c171f8 100644 --- a/src/graph_distances.cpp +++ b/src/graph_distances.cpp @@ -15,7 +15,7 @@ auto graph_distances::empty() const -> bool } auto graph_distances::calculate_distances(const size_t node_count, - const std::vector>& edges, + const std::vector& edges, const std::vector& targets) -> void { // Build a list of adjacent nodes to speed up BFS diff --git a/src/input_handler.cpp b/src/input_handler.cpp index bcd1d5a..a10d856 100644 --- a/src/input_handler.cpp +++ b/src/input_handler.cpp @@ -208,7 +208,7 @@ auto input_handler::add_block() -> void has_block_add_xy = false; } else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) { const std::optional& next = current.try_add_block( - puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false)); + block(block_add_x, block_add_y, block_add_width, block_add_height, false)); if (next) { sel_x = block_add_x; @@ -224,7 +224,7 @@ auto input_handler::add_block() -> void auto input_handler::remove_block() -> void { const puzzle& current = state.get_current_state(); - const std::optional& b = current.try_get_block(hov_x, hov_y); + const std::optional& b = current.try_get_block(hov_x, hov_y); if (!editing || has_block_add_xy || !b) { return; } diff --git a/src/load_save.cpp b/src/load_save.cpp index 8fbc275..ea348fe 100644 --- a/src/load_save.cpp +++ b/src/load_save.cpp @@ -62,7 +62,10 @@ auto append_preset_file(const std::string& preset_file, const std::string& prese return true; } -auto append_preset_file_quiet(const std::string& preset_file, const std::string& preset_name, const puzzle& p, const bool validate) -> bool +auto append_preset_file_quiet(const std::string& preset_file, + const std::string& preset_name, + const puzzle& p, + const bool validate) -> bool { if (validate && p.try_get_invalid_reason()) { return false; diff --git a/src/main.cpp b/src/main.cpp index 933a1f8..b42a7ce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,10 +16,35 @@ namespace po = boost::program_options; #endif -// TODO: Implement state discovery/enumeration -// - Find all possible initial board states (single one for each possible statespace). -// Currently wer're just finding all states given the initial state -// - Would allow to generate random puzzles with a certain move count +// Threadpool setup +#ifdef THREADPOOL +auto set_pool_thread_name(size_t idx) -> void +{ + BS::this_thread::set_os_thread_name(std::format("worker-{}", idx)); +} + +BS::thread_pool<> threads(std::thread::hardware_concurrency() - 2, set_pool_thread_name); +constexpr threadpool thread_pool = &threads; +#else +constexpr threadpool thread_pool = std::nullopt; +#endif + +// Argparse defaults +std::string preset_file = "default.puzzle"; +std::string output_file = "clusters.puzzle"; +int max_blocks = 5; +int min_moves = 10; + +// Puzzle space setup +int board_width; +int board_height; +int goal_x; +int goal_y; +bool restricted; +blockset2 permitted_blocks; +block target_block; +std::tuple target_block_pos_range; + // TODO: Export cluster to graphviz // TODO: Fix naming: // - Target: The block that has to leave the board to win @@ -32,29 +57,6 @@ namespace po = boost::program_options; // TODO: Add state space generation time to debug overlay // TODO: Move selection accordingly when undoing moves (need to diff two states and get the moved blocks) -// TODO: Click states in the graph to display them in the board - -#ifdef THREADPOOL -auto set_pool_thread_name(size_t idx) -> void -{ - BS::this_thread::set_os_thread_name(std::format("worker-{}", idx)); -} - -BS::thread_pool<> threads(std::thread::hardware_concurrency() - 2, set_pool_thread_name); -constexpr std::optional* const> thread_pool = &threads; -#else -constexpr std::optional* const> thread_pool = std::nullopt; -#endif - -std::string preset_file = "default.puzzle"; -std::string output_file = "clusters.puzzle"; -int max_blocks = 5; -int board_width = 6; -int board_height = 6; -int goal_x = 4; -int goal_y = 2; -bool restricted = true; - auto ui_mode() -> int { // RayLib window setup @@ -167,56 +169,87 @@ auto ui_mode() -> int return 0; } -auto rush_hour_puzzle_space() -> int +auto rush_hour_puzzle_space() -> void { - const boost::unordered_flat_set permitted_blocks = { - puzzle::block(0, 0, 2, 1, false, false), - puzzle::block(0, 0, 3, 1, false, false), - puzzle::block(0, 0, 1, 2, false, false), - puzzle::block(0, 0, 1, 3, false, false) - }; - const puzzle::block target_block = puzzle::block(0, 0, 2, 1, true, false); - const std::tuple target_block_pos_range = { - 0, - goal_y, - board_width - target_block.get_width() - 1, - goal_y + board_width = 6; + board_height = 6; + goal_x = 4; + goal_y = 2; + restricted = true; + permitted_blocks = { + block(0, 0, 2, 1, false, false), + block(0, 0, 3, 1, false, false), + block(0, 0, 1, 2, false, false), + block(0, 0, 1, 3, false, false) }; + target_block = block(0, 0, 2, 1, true, false); + target_block_pos_range = {0, goal_y, board_width - target_block.get_width(), goal_y}; +} - infoln("Exploring Rush-Hour puzzle space:"); +auto klotski_puzzle_space() -> void +{ + board_width = 4; + board_height = 5; + goal_x = 1; + goal_y = 3; + restricted = false; + permitted_blocks = { + block(0, 0, 1, 1, false, false), + block(0, 0, 1, 2, false, false), + block(0, 0, 2, 1, false, false), + }; + target_block = block(0, 0, 2, 2, true, false); + target_block_pos_range = { + 0, + 0, + board_width - target_block.get_width(), + board_height - target_block.get_height(), + }; +} + +auto puzzle_space() -> int +{ + // We don't only pick max_blocks out of n (with duplicates), but also 1 out of n, 2, 3, ... max_blocks-1 out of n + int upper_set_count = 0; + for (int i = 1; i <= max_blocks; ++i) { + upper_set_count += binom(permitted_blocks.size() + i - 1, i); + } + + infoln("Exploring puzzle space:"); infoln("- Size: {}x{}", board_width, board_height); infoln("- Goal: {},{}", goal_x, goal_y); infoln("- Restricted: {}", restricted); infoln("- Max Blocks: {}", max_blocks); + infoln("- Min Moves: {}", min_moves); infoln("- Target: {}x{}", target_block.get_width(), target_block.get_height()); + infoln("- Max Sets: {}", upper_set_count); infoln("- Permitted block sizes:"); std::cout << " "; - for (const puzzle::block b : permitted_blocks) { + for (const block b : permitted_blocks) { std::cout << std::format(" {}x{},", b.get_width(), b.get_height()); } std::cout << std::endl; - const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); const puzzle p = puzzle(board_width, board_height, goal_x, goal_y, restricted, true); - const boost::unordered_flat_set result = p.explore_puzzle_space( + + STARTTIME; + const puzzleset result = p.explore_puzzle_space( permitted_blocks, target_block, target_block_pos_range, max_blocks, + min_moves, thread_pool); + ENDTIME(std::format("Found {} different clusters", result.size()), std::chrono::seconds, "s"); - const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); - infoln("Found {} different clusters. Took {}s.", - result.size(), - std::chrono::duration_cast(end - start).count()); + // TODO: The exported clusters are the numerically smallest state of the cluster. + // Not the state with the longest path. infoln("Sorting clusters..."); std::vector result_sorted{result.begin(), result.end()}; std::ranges::sort(result_sorted, std::ranges::greater{}); - // for (const puzzle& _p : result_sorted) { - // traceln("{}", _p.string_repr()); - // } + infoln("Saving clusters..."); size_t i = 0; size_t success = 0; std::filesystem::remove(output_file); @@ -237,26 +270,32 @@ auto rush_hour_puzzle_space() -> int enum class runmode { - USER_INTERFACE, RUSH_HOUR_PUZZLE_SPACE, EXIT, + USER_INTERFACE, + RUSH_HOUR_PUZZLE_SPACE, + KLOTSKI_PUZZLE_SPACE, + EXIT, }; auto argparse(const int argc, char* argv[]) -> runmode { #if not defined(_WIN32) po::options_description desc("Allowed options"); - desc.add_options() // - ("help", "produce help message") // - ("presets", po::value()->default_value(preset_file), "load presets from file") // - ("output", po::value()->default_value(output_file), "output file for generated clusters") // - ("space", po::value()->value_name("rh|klotski"), "generate puzzle space with ruleset") // - ("w", po::value()->default_value(board_width)->value_name("[3, 8]"), "board width") // - ("h", po::value()->default_value(board_height)->value_name("[3, 8"), "board height") // - ("gx", po::value()->default_value(goal_x)->value_name("[0, w-1]"), "board goal horizontal position") // - ("gy", po::value()->default_value(goal_y)->value_name("[0, h-1]"), "board goal vertical position") // - ("free", "allow free block movement") // + desc.add_options() // + ("help", "produce help message") // + ("presets", po::value()->default_value(preset_file), "load presets from file") // + ("output", po::value()->default_value(output_file), "output file for generated clusters") // + ("space", po::value()->value_name("rh|klotski"), "generate puzzle space with ruleset") // + // ("w", po::value()->default_value(board_width)->value_name("[3, 8]"), "board width") // + // ("h", po::value()->default_value(board_height)->value_name("[3, 8"), "board height") // + // ("gx", po::value()->default_value(goal_x)->value_name("[0, w-1]"), "board goal horizontal position") // + // ("gy", po::value()->default_value(goal_y)->value_name("[0, h-1]"), "board goal vertical position") // + // ("free", "allow free block movement") // ("blocks", po::value()->default_value(max_blocks)->value_name("[1, 15]"), "block limit for puzzle space generation") // + ("moves", + po::value()->default_value(min_moves), + "only save puzzles with at least this many required moves") // ; po::positional_options_description positional; @@ -270,52 +309,49 @@ auto argparse(const int argc, char* argv[]) -> runmode std::cout << desc << std::endl; return runmode::EXIT; } - if (vm.contains("output")) { output_file = vm["output"].as(); } - if (vm.contains("w")) { - board_width = vm["w"].as(); - board_width = std::max(static_cast(puzzle::MIN_WIDTH), - std::min(board_width, static_cast(puzzle::MAX_WIDTH))); - } - - if (vm.contains("h")) { - board_height = vm["h"].as(); - board_height = std::max(static_cast(puzzle::MIN_HEIGHT), - std::min(board_height, static_cast(puzzle::MAX_HEIGHT))); - } - - if (vm.contains("gx")) { - goal_x = vm["gx"].as(); - goal_x = std::max(0, std::min(goal_x, static_cast(puzzle::MAX_WIDTH) - 1)); - } - - if (vm.contains("gy")) { - goal_y = vm["gy"].as(); - goal_y = std::max(0, std::min(goal_y, static_cast(puzzle::MAX_HEIGHT) - 1)); - } - - if (vm.contains("free")) { - restricted = false; - } + // if (vm.contains("w")) { + // board_width = vm["w"].as(); + // board_width = std::max(static_cast(puzzle::MIN_WIDTH), + // std::min(board_width, static_cast(puzzle::MAX_WIDTH))); + // } + // if (vm.contains("h")) { + // board_height = vm["h"].as(); + // board_height = std::max(static_cast(puzzle::MIN_HEIGHT), + // std::min(board_height, static_cast(puzzle::MAX_HEIGHT))); + // } + // if (vm.contains("gx")) { + // goal_x = vm["gx"].as(); + // goal_x = std::max(0, std::min(goal_x, static_cast(puzzle::MAX_WIDTH) - 1)); + // } + // if (vm.contains("gy")) { + // goal_y = vm["gy"].as(); + // goal_y = std::max(0, std::min(goal_y, static_cast(puzzle::MAX_HEIGHT) - 1)); + // } + // if (vm.contains("free")) { + // restricted = false; + // } if (vm.contains("blocks")) { max_blocks = vm["blocks"].as(); max_blocks = std::max(1, std::min(max_blocks, static_cast(puzzle::MAX_BLOCKS))); } - + if (vm.contains("moves")) { + min_moves = vm["moves"].as(); + min_moves = std::max(0, min_moves); + } if (vm.contains("space")) { const std::string ruleset = vm["space"].as(); if (ruleset == "rh") { return runmode::RUSH_HOUR_PUZZLE_SPACE; } if (ruleset == "klotski") { - throw std::runtime_error("Not implemented"); + return runmode::KLOTSKI_PUZZLE_SPACE; } } - if (vm.contains("presets")) { preset_file = vm["presets"].as(); } @@ -357,7 +393,11 @@ auto main(const int argc, char* argv[]) -> int case runmode::USER_INTERFACE: return ui_mode(); case runmode::RUSH_HOUR_PUZZLE_SPACE: - return rush_hour_puzzle_space(); + rush_hour_puzzle_space(); + return puzzle_space(); + case runmode::KLOTSKI_PUZZLE_SPACE: + klotski_puzzle_space(); + return puzzle_space(); case runmode::EXIT: return 0; }; diff --git a/src/octree.cpp b/src/octree.cpp index 87312be..b044c27 100644 --- a/src/octree.cpp +++ b/src/octree.cpp @@ -62,8 +62,8 @@ auto octree::build_octree_morton(octree& t, // Container for building the particle list before sorting by morton code struct sort_node { - uint64_t code; - uint32_t id; + u64 code; + u32 id; Vector3 pos; }; @@ -71,7 +71,7 @@ auto octree::build_octree_morton(octree& t, std::vector sort_container; sort_container.resize(positions.size()); - const auto calculate_morton = [&](const uint32_t i) + const auto calculate_morton = [&](const u32 i) { sort_container[i] = {pos_to_morton(positions[i], root_min, root_max), i, positions[i]}; }; @@ -79,7 +79,7 @@ auto octree::build_octree_morton(octree& t, if (thread_pool) { (*thread_pool)->submit_loop(0, positions.size(), calculate_morton, SMALL_TASK_BLOCK_SIZE).wait(); } else { - for (uint32_t i = 0; i < positions.size(); ++i) { + for (u32 i = 0; i < positions.size(); ++i) { calculate_morton(i); } } @@ -154,7 +154,7 @@ auto octree::build_octree_morton(octree& t, // For grouping, store a nodes local index in its level. struct leaf { - uint64_t leaf_code; + u64 leaf_code; int depth; int level_index; }; @@ -177,7 +177,7 @@ auto octree::build_octree_morton(octree& t, size_t i = 0; while (i < leaves.size()) { - const uint64_t key = path_to_ancestor(leaves[i].leaf_code, MAX_DEPTH, current_depth); + const u64 key = path_to_ancestor(leaves[i].leaf_code, MAX_DEPTH, current_depth); size_t j = i + 1; while (j < leaves.size() && path_to_ancestor(leaves[j].leaf_code, MAX_DEPTH, current_depth) == key) { diff --git a/src/orbit_camera.cpp b/src/orbit_camera.cpp index 59fad2e..c1deba1 100644 --- a/src/orbit_camera.cpp +++ b/src/orbit_camera.cpp @@ -31,24 +31,27 @@ auto orbit_camera::pan(const Vector2 last_mouse, const Vector2 mouse) -> void const Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); - const Vector3 offset = - Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed)); + const Vector3 offset = Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed)); target = Vector3Add(target, offset); } -auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center, - const bool lock, const bool mass_center_lock) -> void +auto orbit_camera::update(const Vector3& current_target, + const Vector3& mass_center, + const bool lock, + const bool mass_center_lock) -> void { if (lock) { if (mass_center_lock) { - target = Vector3MoveTowards(target, mass_center, - CAMERA_SMOOTH_SPEED * GetFrameTime() * - Vector3Length(Vector3Subtract(target, mass_center))); + target = Vector3MoveTowards(target, + mass_center, + CAMERA_SMOOTH_SPEED * GetFrameTime() * Vector3Length( + Vector3Subtract(target, mass_center))); } else { - target = Vector3MoveTowards(target, current_target, - CAMERA_SMOOTH_SPEED * GetFrameTime() * - Vector3Length(Vector3Subtract(target, current_target))); + target = Vector3MoveTowards(target, + current_target, + CAMERA_SMOOTH_SPEED * GetFrameTime() * Vector3Length( + Vector3Subtract(target, current_target))); } } diff --git a/src/puzzle.cpp b/src/puzzle.cpp index 3258530..d05d928 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -1,44 +1,43 @@ #include "puzzle.hpp" +#include "graph_distances.hpp" + #include +#include +#include #include -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 +auto block::create_repr(const u8 x, const u8 y, const u8 w, const u8 h, const bool t, const bool i) -> u16 { 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::unpack_repr() const -> std::tuple +auto 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 u8 x = get_x(); + const u8 y = get_y(); + const u8 w = get_width(); + const u8 h = get_height(); const bool t = get_target(); const bool i = get_immovable(); return {x, y, w, h, t, i}; } -auto puzzle::block::hash() const -> size_t +auto block::hash() const -> size_t { - return std::hash{}(repr); + return std::hash{}(repr); } -auto puzzle::block::position_independent_hash() const -> size_t +auto block::position_independent_hash() const -> size_t { - uint16_t r = repr; + u16 r = repr; clear_bits(r, X_S, X_E); clear_bits(r, Y_S, Y_E); - return std::hash{}(r); + return std::hash{}(r); } -auto puzzle::block::valid() const -> bool +auto block::valid() const -> bool { // This means the first bit is set, marking the block as empty if (repr & INVALID) { @@ -54,7 +53,7 @@ auto puzzle::block::valid() const -> bool return w > 0 && h > 0 && x + w <= MAX_WIDTH && y + h <= MAX_HEIGHT; } -auto puzzle::block::principal_dirs() const -> uint8_t +auto block::principal_dirs() const -> u8 { const auto [x, y, w, h, t, i] = unpack_repr(); @@ -71,14 +70,14 @@ auto puzzle::block::principal_dirs() const -> uint8_t return nor | eas | sou | wes; } -auto puzzle::block::covers(const int _x, const int _y) const -> bool +auto block::covers(const int _x, const int _y) const -> bool { 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 block::collides(const block b) const -> bool { const auto [x, y, w, h, t, i] = unpack_repr(); const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); @@ -86,11 +85,11 @@ auto puzzle::block::collides(const block b) const -> bool return x < bx + bw && x + w > bx && y < by + bh && y + h > by; } -auto puzzle::create_meta(const std::tuple& meta) -> uint16_t +auto puzzle::create_meta(const std::tuple& meta) -> u16 { const auto [w, h, gx, gy, r, g] = meta; - uint16_t m = 0; + u16 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); @@ -100,13 +99,13 @@ auto puzzle::create_meta(const std::tuple& b) -> repr_cooked + 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; @@ -114,13 +113,10 @@ auto puzzle::create_repr(const uint8_t w, 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 +auto puzzle::create_repr(const u64 byte_0, const u64 byte_1, const u64 byte_2, const u64 byte_3) -> repr_cooked { repr_u repr{}; - repr.raw = std::array{byte_0, byte_1, byte_2, byte_3}; + repr.raw = std::array{byte_0, byte_1, byte_2, byte_3}; return repr.cooked; } @@ -135,7 +131,7 @@ auto puzzle::create_repr(const std::string& string_repr) -> repr_cooked return *repr; } -auto puzzle::set_blocks(std::array blocks) const -> puzzle +auto puzzle::set_blocks(std::array blocks) const -> puzzle { puzzle p = *this; std::ranges::sort(blocks); @@ -143,12 +139,12 @@ auto puzzle::set_blocks(std::array blocks) const -> puzzle return p; } -auto puzzle::unpack_meta() const -> std::tuple +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 u8 w = get_width(); + const u8 h = get_height(); + const u8 tx = get_goal_x(); + const u8 ty = get_goal_y(); const bool r = get_restricted(); const bool g = get_goal(); @@ -205,17 +201,17 @@ auto puzzle::string_repr() const -> std::string auto puzzle::try_parse_string_repr(const std::string& string_repr) -> std::optional { bool parsed_size = false; - std::pair size{0, 0}; + std::pair size{0, 0}; bool parsed_goal = false; - std::pair goal{0, 0}; + std::pair goal{0, 0}; bool parsed_restricted = false; bool restricted = true; bool parsed_blocks = false; - std::vector bs; - std::array blocks = invalid_blocks(); + std::vector bs; + std::array blocks = invalid_blocks(); const auto digit = [&](const char c) { @@ -225,8 +221,8 @@ auto puzzle::try_parse_string_repr(const std::string& string_repr) -> std::optio // 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)); + u8 w = std::stoi(string_repr.substr(pos + 3, 1)); + u8 h = std::stoi(string_repr.substr(pos + 5, 1)); size = std::make_pair(w, h); parsed_size = true; @@ -236,8 +232,8 @@ auto puzzle::try_parse_string_repr(const std::string& string_repr) -> std::optio // 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)); + u8 gx = std::stoi(string_repr.substr(pos + 3, 1)); + u8 gy = std::stoi(string_repr.substr(pos + 5, 1)); goal = std::make_pair(gx, gy); parsed_goal = true; @@ -260,14 +256,14 @@ auto puzzle::try_parse_string_repr(const std::string& string_repr) -> std::optio }; // 1x1 or 1X1 or 1*1 or _ - const auto parse_block = [&](size_t& pos, const uint8_t x, const uint8_t y) + const auto parse_block = [&](size_t& pos, const u8 x, const u8 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 u8 w = std::stoi(string_repr.substr(pos, 1)); + const u8 h = std::stoi(string_repr.substr(pos + 2, 1)); const bool t = string_repr[pos + 1] == 'X'; const bool i = string_repr[pos + 1] == '*'; @@ -276,11 +272,11 @@ auto puzzle::try_parse_string_repr(const std::string& string_repr) -> std::optio }; // {1x1 _ _} - const auto parse_row = [&](size_t& pos, const uint8_t y) + const auto parse_row = [&](size_t& pos, const u8 y) { - std::vector row; + std::vector row; - uint8_t x = 0; + u8 x = 0; ++pos; // Skip { while (string_repr[pos] != '}') { if (digit(string_repr[pos]) || string_repr[pos] == '_') { @@ -299,13 +295,13 @@ auto puzzle::try_parse_string_repr(const std::string& string_repr) -> std::optio // B:[{1x1 _ _} {2X1 _ _} {_ _ _}] const auto parse_blocks = [&](size_t& pos) { - std::vector rows; + std::vector rows; - uint8_t y = 0; + u8 y = 0; ++pos; // Skip [ while (string_repr[pos] != ']') { if (string_repr[pos] == '{') { - std::vector row = parse_row(pos, y); + std::vector row = parse_row(pos, y); rows.insert(rows.end(), row.begin(), row.end()); ++y; } @@ -418,10 +414,10 @@ auto puzzle::try_get_invalid_reason() const -> std::optional return std::nullopt; } -auto puzzle::block_count() const -> uint8_t +auto puzzle::block_count() const -> u8 { - uint8_t count = 0; - for (const uint16_t b : repr.cooked.blocks) { + u8 count = 0; + for (const u16 b : repr.cooked.blocks) { if (block(b).valid()) { ++count; } @@ -435,7 +431,7 @@ auto puzzle::goal_reached() const -> bool 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 +auto puzzle::try_get_block(const u8 x, const u8 y) const -> std::optional { if (!covers(x, y)) { return std::nullopt; @@ -461,12 +457,12 @@ auto puzzle::try_get_target_block() const -> std::optional return std::nullopt; } -auto puzzle::covers(const uint8_t x, const uint8_t y, const uint8_t _w, const uint8_t _h) const -> bool +auto puzzle::covers(const u8 x, const u8 y, const u8 _w, const u8 _h) const -> bool { return x + _w <= get_width() && y + _h <= get_height(); } -auto puzzle::covers(const uint8_t x, const uint8_t y) const -> bool +auto puzzle::covers(const u8 x, const u8 y) const -> bool { return covers(x, y, 1, 1); } @@ -481,7 +477,7 @@ auto puzzle::toggle_restricted() const -> puzzle return set_restricted(!get_restricted()); } -auto puzzle::try_set_goal(const uint8_t x, const uint8_t y) const -> std::optional +auto puzzle::try_set_goal(const u8 x, const u8 y) const -> std::optional { const std::optional& b = try_get_target_block(); if (!b || !covers(x, y, b->get_width(), b->get_height())) { @@ -502,7 +498,7 @@ auto puzzle::clear_goal() const -> puzzle auto puzzle::try_add_column() const -> std::optional { - const uint8_t w = get_width(); + const u8 w = get_width(); if (w >= MAX_WIDTH) { return std::nullopt; } @@ -517,7 +513,7 @@ auto puzzle::try_remove_column() const -> std::optional return std::nullopt; } - puzzle p{static_cast(w - 1), h, 0, 0, r, g}; + puzzle p{static_cast(w - 1), h, 0, 0, r, g}; // Re-add all the blocks, blocks no longer fitting won't be added for (const block b : block_view()) { @@ -541,7 +537,7 @@ auto puzzle::try_remove_column() const -> std::optional auto puzzle::try_add_row() const -> std::optional { - const uint8_t h = get_height(); + const u8 h = get_height(); if (h >= MAX_HEIGHT) { return std::nullopt; } @@ -556,7 +552,7 @@ auto puzzle::try_remove_row() const -> std::optional return std::nullopt; } - puzzle p{w, static_cast(h - 1), gx, gy, r, g}; + puzzle p{w, static_cast(h - 1), gx, gy, r, g}; // Re-add all the blocks, blocks no longer fitting won't be added for (const block b : block_view()) { @@ -580,7 +576,7 @@ auto puzzle::try_remove_row() const -> std::optional auto puzzle::try_add_block(const block b) const -> std::optional { - const uint8_t count = block_count(); + const u8 count = block_count(); if (count == MAX_BLOCKS) { return std::nullopt; } @@ -596,7 +592,7 @@ auto puzzle::try_add_block(const block b) const -> std::optional } const auto [w, h, gx, gy, r, g] = unpack_meta(); - std::array blocks = repr.cooked.blocks; + std::array blocks = repr.cooked.blocks; // 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. @@ -605,7 +601,7 @@ auto puzzle::try_add_block(const block b) const -> std::optional return puzzle(w, h, gx, gy, r, g, blocks); } -auto puzzle::try_remove_block(const uint8_t x, const uint8_t y) const -> std::optional +auto puzzle::try_remove_block(const u8 x, const u8 y) const -> std::optional { const std::optional& b = try_get_block(x, y); if (!b) { @@ -613,8 +609,8 @@ auto puzzle::try_remove_block(const uint8_t x, const uint8_t y) const -> std::op } const auto [w, h, gx, gy, r, g] = unpack_meta(); - std::array blocks = repr.cooked.blocks; - for (uint16_t& _b : blocks) { + std::array blocks = repr.cooked.blocks; + for (u16& _b : blocks) { if (_b == b->repr) { _b = block().repr; } @@ -623,7 +619,7 @@ auto puzzle::try_remove_block(const uint8_t x, const uint8_t y) const -> std::op return puzzle(w, h, gx, gy, r, g, blocks); } -auto puzzle::try_toggle_target(const uint8_t x, const uint8_t y) const -> std::optional +auto puzzle::try_toggle_target(const u8 x, const u8 y) const -> std::optional { const std::optional b = try_get_block(x, y); if (!b || b->get_immovable()) { @@ -631,9 +627,9 @@ auto puzzle::try_toggle_target(const uint8_t x, const uint8_t y) const -> std::o } const auto [w, h, gx, gy, r, g] = unpack_meta(); - std::array blocks = repr.cooked.blocks; + std::array blocks = repr.cooked.blocks; - for (uint16_t& _b : blocks) { + for (u16& _b : blocks) { if (!block(_b).valid()) { // Empty blocks are at the end break; @@ -657,7 +653,7 @@ auto puzzle::try_toggle_target(const uint8_t x, const uint8_t y) const -> std::o return puzzle(w, h, 0, 0, r, g, blocks); } -auto puzzle::try_toggle_wall(const uint8_t x, const uint8_t y) const -> std::optional +auto puzzle::try_toggle_wall(const u8 x, const u8 y) const -> std::optional { const std::optional b = try_get_block(x, y); if (!b || b->get_target()) { @@ -665,9 +661,9 @@ auto puzzle::try_toggle_wall(const uint8_t x, const uint8_t y) const -> std::opt } const auto [w, h, gx, gy, r, g] = unpack_meta(); - std::array blocks = repr.cooked.blocks; + std::array blocks = repr.cooked.blocks; - for (uint16_t& _b : blocks) { + for (u16& _b : blocks) { if (!block(_b).valid()) { // Empty blocks are at the end break; @@ -682,17 +678,17 @@ auto puzzle::try_toggle_wall(const uint8_t x, const uint8_t y) const -> std::opt return puzzle(w, h, gx, gy, r, g, blocks); } -auto puzzle::blocks_bitmap() const -> uint64_t +auto puzzle::blocks_bitmap() const -> u64 { - uint64_t bitmap = 0; - for (uint8_t i = 0; i < MAX_BLOCKS; ++i) { + u64 bitmap = 0; + for (u8 i = 0; i < MAX_BLOCKS; ++i) { block b(repr.cooked.blocks[i]); if (!b.valid()) { break; } auto [x, y, w, h, t, im] = b.unpack_repr(); - const uint8_t width = get_width(); + const u8 width = get_width(); for (int dy = 0; dy < h; ++dy) { for (int dx = 0; dx < w; ++dx) { @@ -703,10 +699,10 @@ auto puzzle::blocks_bitmap() const -> uint64_t return bitmap; } -auto puzzle::blocks_bitmap_h() const -> uint64_t +auto puzzle::blocks_bitmap_h() const -> u64 { - uint64_t bitmap = 0; - for (uint8_t i = 0; i < MAX_BLOCKS; ++i) { + u64 bitmap = 0; + for (u8 i = 0; i < MAX_BLOCKS; ++i) { block b(repr.cooked.blocks[i]); if (!b.valid()) { break; @@ -717,7 +713,7 @@ auto puzzle::blocks_bitmap_h() const -> uint64_t } auto [x, y, w, h, t, im] = b.unpack_repr(); - const uint8_t width = get_width(); + const u8 width = get_width(); for (int dy = 0; dy < h; ++dy) { for (int dx = 0; dx < w; ++dx) { @@ -728,10 +724,10 @@ auto puzzle::blocks_bitmap_h() const -> uint64_t return bitmap; } -auto puzzle::blocks_bitmap_v() const -> uint64_t +auto puzzle::blocks_bitmap_v() const -> u64 { - uint64_t bitmap = 0; - for (uint8_t i = 0; i < MAX_BLOCKS; ++i) { + u64 bitmap = 0; + for (u8 i = 0; i < MAX_BLOCKS; ++i) { block b(repr.cooked.blocks[i]); if (!b.valid()) { break; @@ -742,7 +738,7 @@ auto puzzle::blocks_bitmap_v() const -> uint64_t } auto [x, y, w, h, t, im] = b.unpack_repr(); - const uint8_t width = get_width(); + const u8 width = get_width(); for (int dy = 0; dy < h; ++dy) { for (int dx = 0; dx < w; ++dx) { @@ -753,35 +749,7 @@ auto puzzle::blocks_bitmap_v() const -> uint64_t return bitmap; } -auto puzzle::bitmap_find_first_empty(const uint64_t bitmap, int& x, int& y) const -> bool -{ - x = 0; - y = 0; - - // Bitmap is empty of first slot is empty - if (bitmap_is_empty(bitmap) || !(bitmap & 1u)) { - return true; - } - - // Bitmap is full - if (bitmap_is_full(bitmap)) { - return false; - } - - // Find the next more significant empty bit (we know the first slot is full) - int ls_set = 0; - bool next_set = true; - while (next_set && ls_set < get_width() * get_height() - 1) { - next_set = bitmap & (1ul << (ls_set + 1)); - ++ls_set; - } - - x = ls_set % get_width(); - y = ls_set / get_width(); - return true; -} - -auto puzzle::try_move_block_at(const uint8_t x, const uint8_t y, const dir dir) const -> std::optional +auto puzzle::try_move_block_at(const u8 x, const u8 y, const dir dir) const -> std::optional { const std::optional b = try_get_block(x, y); const auto [bx, by, bw, bh, bt, bi] = b->unpack_repr(); @@ -843,45 +811,15 @@ auto puzzle::try_move_block_at(const uint8_t x, const uint8_t y, const dir dir) return p; } -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::explore_state_space() const -> std::pair, std::vector>> +auto puzzle::explore_state_space() const -> std::pair, std::vector> { std::vector state_pool; - boost::unordered_flat_map state_indices; - std::vector> links; + puzzlemap state_indices; + std::vector links; // Buffer for all states we want to call GetNextStates() on std::vector queue; // indices into state_pool - #ifdef WIP - // Store an index to the blocks array of a state for each occupied bitmap cell - std::array bitmap_block_indices; - #endif - // Start with the current state state_indices.emplace(*this, 0); state_pool.push_back(*this); @@ -894,24 +832,6 @@ auto puzzle::explore_state_space() const -> std::pair, std:: // Make a copy because references might be invalidated when inserting into the vector const puzzle current = state_pool[current_idx]; - #ifdef WIP - // Build bitmap-block indices - for (size_t i = 0; i < MAX_BLOCKS; ++i) { - const block b = block(current.repr.cooked.blocks[i]); - const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); - if (!b.valid()) { - break; - } - - for (uint8_t x = bx; x < bx + bw; ++x) { - for (uint8_t y = by; y < by + bh; ++y) { - bitmap_block_indices[y * current.get_width() + x] = i; - } - } - } - #endif - - // TODO: I can just dispatch different functions depending on if the board is restricted or contains walls current.for_each_adjacent([&](const puzzle& p) { auto [it, inserted] = state_indices.emplace(p, state_pool.size()); @@ -926,160 +846,140 @@ auto puzzle::explore_state_space() const -> std::pair, std:: return {std::move(state_pool), std::move(links)}; } -auto puzzle::get_cluster_id_and_solution() const -> std::pair +auto puzzle::get_cluster_id_and_solution() const -> std::tuple, bool, int> { const auto& [puzzles, moves] = explore_state_space(); - bool solution = false; + + std::vector solutions; puzzle min = puzzles[0]; for (size_t i = 0; i < puzzles.size(); ++i) { if (puzzles[i] < min) { min = puzzles[i]; } if (puzzles[i].goal_reached()) { - solution = true; + solutions.emplace_back(i); } } - return {min, solution}; + + // TODO: We only need to search until a distance equal to min_moves is found. + // Maybe DFS instead of BFS could even be faster on average? + // TODO: Split moves from solvable calculation. We only need to count the moves if the cluster is solvable at all. + graph_distances distances; + distances.calculate_distances(puzzles.size(), moves, solutions); + + int max_distance = 0; + for (size_t i = 0; i < distances.distances.size(); ++i) { + if (distances.distances[i] > max_distance) { + max_distance = distances.distances[i]; + } + } + + return {min, puzzles, !solutions.empty(), max_distance}; } -auto puzzle::generate_block_sequences( - const boost::unordered_flat_set& permitted_blocks, - const block target_block, - const size_t max_blocks, - std::vector& current_sequence, - const int current_area, - const int board_area, - const std::function&)>& callback) -> void +auto puzzle::generate_block_sets(const puzzle& p, + const blockset2& permitted_blocks, + const block target_block, + const u8 max_blocks) -> std::vector> { - if (!current_sequence.empty()) { - callback(current_sequence); + std::vector> result; + + const u8 board_area = p.get_width() * p.get_height(); + blockmap2 max_counts; + + blockmap2 block_areas; + for (const block& b : permitted_blocks) { + const u8 block_area = b.get_width() * b.get_height(); + block_areas[b] = block_area; } - if (current_sequence.size() == max_blocks) { - return; + const u8 target_area = target_block.get_width() * target_block.get_height(); + for (const block& b : permitted_blocks) { + const u8 block_area = block_areas[b]; + max_counts[b] = std::min(max_blocks, static_cast((board_area - target_area) / block_area)); } - for (const block b : permitted_blocks) { - const int new_area = current_area + b.get_width() * b.get_height(); - if (new_area > board_area) { - continue; + blockmap2 current_set; + while (true) { + // Check if we have generated a valid set + int used_blocks = 0; + int used_area = 0; + for (const block& b : permitted_blocks) { + used_blocks += current_set[b]; + used_area += current_set[b] * block_areas[b]; + } + if (used_blocks > 0 && used_blocks <= max_blocks && used_area <= board_area - target_area) { + result.push_back(current_set); } - // Explore all sequences with the block placed, then continue the loop - current_sequence.push_back(b); - generate_block_sequences(permitted_blocks, - target_block, - max_blocks, - current_sequence, - new_area, - board_area, - callback); - current_sequence.pop_back(); - } -} - -auto puzzle::place_block_sequence(const puzzle& p, - const uint64_t& bitmap, - const std::tuple& p_repr, - const std::vector& sequence, - const block target_block, - const std::tuple& target_block_pos_range, - const bool has_target, - const size_t index, - const std::function& callback) -> void -{ - if (index == sequence.size()) { - // All blocks placed - callback(p); - return; - } - - if (!has_target && p.get_restricted()) { - // Place target block (restricted movement) - const auto [txs, tys, txe, tye] = target_block_pos_range; - for (int tx = txs; tx <= txe; ++tx) { - for (int ty = tys; ty <= tye; ++ty) { - block t = target_block; - t = t.set_x(tx); - t = t.set_y(ty); - - if (!p.covers(t)) { - continue; - } - - const std::array blocks = sorted_replace(p.repr.cooked.blocks, 0, t.repr); - const puzzle next_p = puzzle(p_repr, blocks); - - uint64_t next_bm = bitmap; - next_p.bitmap_set_block(next_bm, t); - - // Place the remaining blocks for each possible target block configuration - // traceln("Generating block sequence for target at {},{}", tx, ty); - next_p.place_block_sequence(next_p, - next_bm, - p_repr, - sequence, - target_block, - target_block_pos_range, - true, - index, - callback); + // Increase counter + size_t pos = 0; + for (const block& b : permitted_blocks) { + ++current_set[b]; + if (current_set[b] <= max_counts[b]) { + break; } + + // The counter overflowed + current_set[b] = 0; + ++pos; } - return; + // All counters overflowed, finished + if (pos == permitted_blocks.size()) { + break; + } } - - if (!has_target && !p.get_restricted()) { - // Place target block (free movement) - - // TODO - } - - int x, y; - if (!p.bitmap_find_first_empty(bitmap, x, y)) { - // No space remaining - callback(p); - return; - } - - block b = sequence[index]; - b = b.set_x(static_cast(x)); - b = b.set_y(static_cast(y)); - - // Place the next block and call the resulting subtree, then remove the block and continue here - if (!p.bitmap_check_collision(bitmap, b) && p.covers(b)) { - // Shift the sequence by 1 (index + 1), because the target block is inserted separately - const std::array blocks = sorted_replace(p.repr.cooked.blocks, index + 1, b.repr); - const puzzle next_p = puzzle(p_repr, blocks); - - uint64_t next_bm = bitmap; - next_p.bitmap_set_block(next_bm, b); - - next_p.place_block_sequence(next_p, - next_bm, - p_repr, - sequence, - target_block, - target_block_pos_range, - true, - index + 1, - callback); - } - - // Create an empty cell and call the resulting subtree (without advancing the block index) - uint64_t next_bm = bitmap; - bitmap_set_bit(next_bm, p.get_width(), b.get_x(), b.get_y()); - p.place_block_sequence(p, next_bm, p_repr, sequence, target_block, target_block_pos_range, true, index, callback); + return result; } -auto puzzle::explore_puzzle_space(const boost::unordered_flat_set& permitted_blocks, +auto puzzle::generate_initial_puzzles(const puzzle& p, + const block target_block, + const std::tuple& target_block_pos_range) -> std::vector +{ + std::vector result; + + const auto [txs, tys, txe, tye] = target_block_pos_range; + + for (int tx = txs; tx <= txe; ++tx) { + for (int ty = tys; ty <= tye; ++ty) { + block t = target_block; + t = t.set_x(tx); + t = t.set_y(ty); + + // This cannot happen if the target_block_pos_range is set correctly + #ifdef RUNTIME_CHECKS + if (!p.covers(t)) { + continue; + } + #endif + + std::array blocks = invalid_blocks(); + blocks[0] = t.repr; + + // Don't exclude already won configurations, as min_moves is based on the + // max distance from the entire cluster, not only the initial state + + // puzzle _p = puzzle(p.repr.cooked.meta, blocks); + // if (_p.goal_reached()) { + // continue; + // } + // result.emplace_back(_p); + + result.emplace_back(p.repr.cooked.meta, blocks); + } + } + + return result; +} + +auto puzzle::explore_puzzle_space(const blockset2& permitted_blocks, const block target_block, - const std::tuple& target_block_pos_range, + const std::tuple& target_block_pos_range, const size_t max_blocks, - const std::optional* const> thread_pool) const -> - boost::unordered_flat_set + const int min_moves, + const threadpool thread_pool) const -> puzzleset { const auto [w, h, gx, gy, r, g] = unpack_meta(); @@ -1091,43 +991,117 @@ auto puzzle::explore_puzzle_space(const boost::unordered_flat_set visited_clusters; - - // TODO: Can't even parallelize this. Or just start at different initial puzzles? + std::mutex cache_mtx; + std::mutex print_mtx; + puzzleset visited_clusters; const puzzle empty_puzzle = puzzle(w, h, gx, gy, r, g); - const auto board_repr = std::make_tuple(w, h, gx, gy, r, g); - std::vector current_sequence; - int total = 0; - generate_block_sequences(permitted_blocks, - target_block, - max_blocks - 1, // Make space for the target block - current_sequence, - target_block.get_width() * target_block.get_height(), // Starting area - get_width() * get_height(), - [&](const std::vector& sequence) - { - place_block_sequence(empty_puzzle, - 0, - board_repr, - sequence, - target_block, - target_block_pos_range, - false, - 0, - [&](const puzzle& p) - { - const auto [cluster_id, winnable] = p.get_cluster_id_and_solution(); - std::lock_guard lock(mtx); - ++total; - if (winnable) { - visited_clusters.emplace(cluster_id); - } - }); - }); - // infoln("Found {} of {} clusters with a solution", visited_clusters.size(), total); + // TODO: Don't use a hashmap for this, use denser vector representation for better recursion performance + // Call with max_blocks - 1 because the target is already placed + const std::vector> sets = generate_block_sets(empty_puzzle, + permitted_blocks, + target_block, + max_blocks - 1); + traceln("Generated {} block inventories", sets.size()); + + const std::vector puzzles = generate_initial_puzzles(empty_puzzle, target_block, target_block_pos_range); + traceln("Generated {} starting configurations", puzzles.size()); + + int total = 0; + int duplicates = 0; + + // TODO: This cache can easily fill system ram + // - Only store hashes, not puzzles? + // - Use an LRU cache to have a memory consumption bound? + puzzleset seen_statespaces; + + const auto place_blocks = [&](const size_t i) + { + const size_t current_inventory = i / puzzles.size(); + const size_t current_puzzle = i % puzzles.size(); + + const blockmap2 set = sets[current_inventory]; + const puzzle& p = puzzles[current_puzzle]; + const u64 bitmap = p.blocks_bitmap(); + + { + std::lock_guard lock(print_mtx); + traceln("Placing inventory {} for starting configuration {}", current_inventory, current_puzzle); + } + place_block_set(p, + set, + bitmap, + 1, + [&](const puzzle& _p) + { + // Sort the blocks, so identical puzzles get the same hash + const puzzle& sorted = _p.set_blocks(_p.repr.cooked.blocks); + + // TODO: Pre-filter using a coarse key: + // - If two puzzles are in the same cluster, this key must match + // - If the key matches, puzzles might still be in different clusters + // If a new puzzle arrives, do a bidirectional BFS from the new + // puzzle to all puzzles with the same key, if both directions meet, + // we already had this cluster. + // If nothing meets, we have to do the full statespace generation. + // + // Key could contain: + // - counts of block types (inventory) + // - which row/column pieces live in + // - ordering of pieces in a row or column + + bool inserted; + { + std::lock_guard lock(cache_mtx); + const auto& [it, _inserted] = seen_statespaces.insert(sorted); + inserted = _inserted; + + ++total; + } + + // Only scan statespace if we haven't seen the state + if (!inserted) { + return; + } + + // TODO: This is the main issue. Need a lot of prefiltering/as much caching as I have memory + const auto [cluster_id, statespace, winnable, max_moves] = sorted. + get_cluster_id_and_solution(); + + { + std::lock_guard lock(cache_mtx); + for (const puzzle& seen : statespace) { + seen_statespaces.insert(seen); + } + } + + { + std::lock_guard lock(mtx); + if (visited_clusters.contains(cluster_id)) { + ++duplicates; + } else if (winnable && max_moves >= min_moves) { + // print_bitmap(cluster_id.blocks_bitmap(), _p.get_width(), _p.get_height(), "Found Cluster"); + visited_clusters.emplace(cluster_id); + } + } + }); + }; + + // Iterate through all starting configurations + inventories + if (thread_pool) { + (*thread_pool)->submit_loop(0, puzzles.size() * sets.size(), place_blocks, LARGE_TASK_BLOCK_SIZE).wait(); + } else { + for (size_t i = 0; i < puzzles.size() * sets.size(); ++i) { + place_blocks(i); + } + } + + infoln("Found {} solvable clusters with at least {} required moves", visited_clusters.size(), min_moves); + traceln("- Scanned {} puzzles", total); + traceln("- {} belong to duplicate clusters", duplicates); + traceln("- {} don't match the requirements", total - duplicates - visited_clusters.size()); return visited_clusters; } \ No newline at end of file diff --git a/src/renderer.cpp b/src/renderer.cpp index f24f95f..19b7cc3 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -128,10 +128,10 @@ auto renderer::draw_mass_springs(const std::vector& masses) -> void const float weight = 1.0 - static_cast(distance) / max_distance; Color result; - result.r = static_cast((1 - weight) * from.r + weight * to.r); - result.g = static_cast((1 - weight) * from.g + weight * to.g); - result.b = static_cast((1 - weight) * from.b + weight * to.b); - result.a = static_cast((1 - weight) * from.a + weight * to.a); + result.r = static_cast((1 - weight) * from.r + weight * to.r); + result.g = static_cast((1 - weight) * from.g + weight * to.g); + result.b = static_cast((1 - weight) * from.b + weight * to.b); + result.a = static_cast((1 - weight) * from.a + weight * to.a); return result; }; diff --git a/src/state_manager.cpp b/src/state_manager.cpp index b681bce..901af9b 100644 --- a/src/state_manager.cpp +++ b/src/state_manager.cpp @@ -28,7 +28,7 @@ auto state_manager::synced_insert_link(size_t first_index, size_t second_index) } auto state_manager::synced_insert_statespace(const std::vector& states, - const std::vector>& _links) -> void + const std::vector& _links) -> void { if (!state_pool.empty() || !state_indices.empty() || !links.empty()) { warnln("Inserting statespace but collections haven't been cleared"); @@ -374,7 +374,7 @@ auto state_manager::get_path_length() const -> size_t return winning_path.size(); } -auto state_manager::get_links() const -> const std::vector>& +auto state_manager::get_links() const -> const std::vector& { return links; } diff --git a/src/user_interface.cpp b/src/user_interface.cpp index 2ed907d..12a5a55 100644 --- a/src/user_interface.cpp +++ b/src/user_interface.cpp @@ -7,8 +7,12 @@ #define RAYGUI_IMPLEMENTATION #include -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; @@ -51,7 +55,9 @@ auto user_interface::grid::bounds(const int _x, const int _y, const int _width, 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, + return Rectangle(x + _x * cell_width + padding, + y + _y * cell_height + padding, + _width * cell_width - padding, _height * cell_height - padding); } @@ -65,7 +71,9 @@ auto user_interface::grid::square_bounds() const -> Rectangle return bounds; } -auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width, +auto user_interface::grid::square_bounds(const int _x, + const int _y, + const int _width, const int _height) const -> Rectangle { // Assumes each cell is square, so either width or height are not completely @@ -84,8 +92,10 @@ 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 @@ -146,16 +156,26 @@ auto user_interface::get_default_style() -> default_style // 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, 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) + 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) }; } @@ -191,14 +211,21 @@ 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_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, BORDER_WIDTH), + GuiGetStyle(component, TEXT_PADDING), GuiGetStyle(component, TEXT_ALIGNMENT) }; } @@ -229,11 +256,16 @@ 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(); @@ -263,16 +295,25 @@ auto user_interface::draw_button(const Rectangle bounds, const std::string& labe return pressed; } -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, +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 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, +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 { // Save original styling @@ -308,16 +349,29 @@ 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(); @@ -352,15 +406,26 @@ auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, in return pressed; } -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 +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 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, +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 @@ -391,7 +456,11 @@ 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, +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); @@ -400,7 +469,7 @@ auto user_interface::draw_board_block(const int x, const int y, const int width, const Rectangle bounds = board_grid.square_bounds(x, y, width, height); const bool focused = CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds); - const bool pressed = puzzle::block(x, y, width, height, false).covers(input.sel_x, input.sel_y); + const bool pressed = block(x, y, width, height, false).covers(input.sel_x, input.sel_y); // Background to make faded colors work DrawRectangleRec(bounds, RAYWHITE); @@ -438,7 +507,15 @@ 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, + 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(); @@ -446,11 +523,17 @@ auto user_interface::draw_menu_header(const Color color) const -> void input.load_previous_preset(); } - 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); + 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); @@ -461,13 +544,22 @@ 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(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 @@ -504,7 +596,14 @@ auto user_interface::draw_camera_controls(const Color color) const -> void } 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, + 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(); @@ -537,8 +636,12 @@ 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(); @@ -556,7 +659,12 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void input.goto_most_distant_state(); } - if (draw_menu_button(2, 5, 1, 1, "Go to Starting State (R)", color, + if (draw_menu_button(2, + 5, + 1, + 1, + "Go to Starting State (R)", + color, state.get_current_index() != state.get_starting_index())) { input.goto_starting_state(); } @@ -634,8 +742,13 @@ 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_comment.data(), 255, nullptr); + const int button = GuiTextInputBox(popup_bounds(), + "Save as Preset", + "Enter Preset Name", + "Ok;Cancel", + preset_comment.data(), + 255, + nullptr); if (button == 1) { state.save_current_to_preset_file(preset_comment.data()); } @@ -698,7 +811,11 @@ 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.get_width(), + board_grid.update_bounds(0, + MENU_HEIGHT, + GetScreenWidth() / 2, + GetScreenHeight() - MENU_HEIGHT, + current.get_width(), current.get_height()); // Draw outer border @@ -706,18 +823,22 @@ auto user_interface::draw_puzzle_board() -> void 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, + 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 std::optional target_block = current.try_get_target_block(); 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(), + 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.goal_reached() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3); @@ -764,7 +885,7 @@ auto user_interface::draw_puzzle_board() -> void } // Draw blocks - for (const puzzle::block b : current.block_view()) { + for (const block b : current.block_view()) { Color c = BLOCK_COLOR; if (b.get_target()) { c = TARGET_BLOCK_COLOR; @@ -782,16 +903,22 @@ auto user_interface::draw_puzzle_board() -> void 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.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))) { + for (const block b : current.block_view()) { + if (b.collides(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, - input.hov_y - input.block_add_y + 1, PURPLE); + 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); } } } @@ -803,7 +930,8 @@ auto user_interface::draw_puzzle_board() -> void // Draw goal boundaries when editing 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, + board_grid.square_bounds(target_x, target_y, target_block->get_width(), target_block->get_height()), + 2.0, TARGET_BLOCK_COLOR); } } diff --git a/test/bitmap.cpp b/test/bitmap.cpp index 50fd64e..de33d60 100644 --- a/test/bitmap.cpp +++ b/test/bitmap.cpp @@ -10,7 +10,7 @@ TEST_CASE("bitmap_is_full all bits set", "[puzzle][board]") puzzle p2(3, 4); puzzle p3(5, 4); puzzle p4(3, 7); - uint64_t bitmap = -1; + u64 bitmap = -1; REQUIRE(p1.bitmap_is_full(bitmap)); REQUIRE(p2.bitmap_is_full(bitmap)); @@ -24,7 +24,7 @@ TEST_CASE("bitmap_is_full no bits set", "[puzzle][board]") puzzle p2(3, 4); puzzle p3(5, 4); puzzle p4(3, 7); - uint64_t bitmap = 0; + u64 bitmap = 0; REQUIRE_FALSE(p1.bitmap_is_full(bitmap)); REQUIRE_FALSE(p2.bitmap_is_full(bitmap)); @@ -39,10 +39,10 @@ TEST_CASE("bitmap_is_full necessary bits set", "[puzzle][board]") puzzle p3(5, 4); puzzle p4(3, 7); - uint64_t bitmap1 = (1ull << 25) - 1; // 5 * 5 - uint64_t bitmap2 = (1ull << 12) - 1; // 3 * 4 - uint64_t bitmap3 = (1ull << 20) - 1; // 5 * 4 - uint64_t bitmap4 = (1ull << 21) - 1; // 3 * 7 + u64 bitmap1 = (1ull << 25) - 1; // 5 * 5 + u64 bitmap2 = (1ull << 12) - 1; // 3 * 4 + u64 bitmap3 = (1ull << 20) - 1; // 5 * 4 + u64 bitmap4 = (1ull << 21) - 1; // 3 * 7 REQUIRE(p1.bitmap_is_full(bitmap1)); REQUIRE(p2.bitmap_is_full(bitmap2)); @@ -57,10 +57,10 @@ TEST_CASE("bitmap_is_full necessary bits not set", "[puzzle][board]") puzzle p3(5, 4); puzzle p4(3, 7); - uint64_t bitmap1 = (1ull << 25) - 1; // 5 * 5 - uint64_t bitmap2 = (1ull << 12) - 1; // 3 * 4 - uint64_t bitmap3 = (1ull << 20) - 1; // 5 * 4 - uint64_t bitmap4 = (1ull << 21) - 1; // 3 * 7 + u64 bitmap1 = (1ull << 25) - 1; // 5 * 5 + u64 bitmap2 = (1ull << 12) - 1; // 3 * 4 + u64 bitmap3 = (1ull << 20) - 1; // 5 * 4 + u64 bitmap4 = (1ull << 21) - 1; // 3 * 7 bitmap1 &= ~(1ull << 12); bitmap2 &= ~(1ull << 6); diff --git a/test/bitmap_find_first_empty.cpp b/test/bitmap_find_first_empty.cpp index 8af616e..7d6fc4f 100644 --- a/test/bitmap_find_first_empty.cpp +++ b/test/bitmap_find_first_empty.cpp @@ -5,7 +5,7 @@ #include #include -static auto board_mask(const int w, const int h) -> uint64_t +static auto board_mask(const int w, const int h) -> u64 { const int cells = w * h; if (cells == 64) { @@ -31,7 +31,7 @@ TEST_CASE("Full board detection respects width*height only", "[puzzle][board]") puzzle p(w, h); - uint64_t mask = board_mask(w, h); + u64 mask = board_mask(w, h); int x = -1, y = -1; @@ -51,7 +51,7 @@ TEST_CASE("Single empty cell at various positions", "[puzzle][board]") auto empty_index = GENERATE_COPY(values({ 0, cells / 2, cells - 1})); - uint64_t bitmap = board_mask(w, h); + u64 bitmap = board_mask(w, h); bitmap &= ~(1ULL << empty_index); int x = -1, y = -1; @@ -65,10 +65,10 @@ TEST_CASE("Bits outside board are ignored", "[puzzle][board]") { puzzle p(3, 3); // 9 cells - uint64_t mask = board_mask(3, 3); + u64 mask = board_mask(3, 3); // Board is full - uint64_t bitmap = mask; + u64 bitmap = mask; // Set extra bits outside 9 cells bitmap |= (1ULL << 20); @@ -83,7 +83,7 @@ TEST_CASE("First empty found in forward search branch", "[puzzle][branch]") puzzle p(4, 4); // 16 cells // Only MSB (within board) set - uint64_t bitmap = (1ULL << 15); + u64 bitmap = (1ULL << 15); int x = -1, y = -1; REQUIRE(p.bitmap_find_first_empty(bitmap, x, y)); @@ -97,7 +97,7 @@ TEST_CASE("Backward search branch finds gap before MSB cluster", "[puzzle][branc puzzle p(4, 4); // 16 cells // Set bits 15,14,13 but leave 12 empty - uint64_t bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13); + u64 bitmap = (1ULL << 15) | (1ULL << 14) | (1ULL << 13); int x = -1, y = -1; REQUIRE(p.bitmap_find_first_empty(bitmap, x, y)); @@ -112,7 +112,7 @@ TEST_CASE("Rectangular board coordinate mapping", "[puzzle][rect]") int empty_index = 11; - uint64_t bitmap = board_mask(5, 3); + u64 bitmap = board_mask(5, 3); bitmap &= ~(1ULL << empty_index); int x = -1, y = -1; @@ -126,7 +126,7 @@ TEST_CASE("Non-64-sized board near limit", "[puzzle][limit]") { puzzle p(7, 8); // 56 cells - uint64_t mask = board_mask(7, 8); + u64 mask = board_mask(7, 8); // Full board should return false int x = -1, y = -1; @@ -142,7 +142,7 @@ TEST_CASE("Non-64-sized board near limit", "[puzzle][limit]") } // --- Oracle: find first zero bit inside board --- -static auto oracle_find_first_empty(uint64_t bitmap, int w, int h, int& x, int& y) -> bool +static auto oracle_find_first_empty(u64 bitmap, int w, int h, int& x, int& y) -> bool { int cells = w * h; @@ -164,13 +164,13 @@ TEST_CASE("Oracle validation across board sizes 3x3 to 8x8", "[puzzle][oracle]") puzzle p(w, h); - uint64_t mask = board_mask(w, h); + u64 mask = board_mask(w, h); std::mt19937_64 rng(12345); - std::uniform_int_distribution dist(0, UINT64_MAX); + std::uniform_int_distribution dist(0, UINT64_MAX); for (int iteration = 0; iteration < 200; ++iteration) { - uint64_t bitmap = dist(rng); + u64 bitmap = dist(rng); int ox = -1, oy = -1; bool oracle_result = oracle_find_first_empty(bitmap, w, h, ox, oy); @@ -191,7 +191,7 @@ TEST_CASE("Bits set outside board only behaves as empty board", "[puzzle][outsid { puzzle p(3, 3); // 9 cells - uint64_t bitmap = (1ULL << 40) | (1ULL << 63); + u64 bitmap = (1ULL << 40) | (1ULL << 63); int x = -1, y = -1; REQUIRE(p.bitmap_find_first_empty(bitmap, x, y)); @@ -207,7 +207,7 @@ TEST_CASE("Last valid cell empty stresses upper bound", "[puzzle][boundary]") puzzle p(w, h); int cells = w * h; - uint64_t bitmap = board_mask(w, h); + u64 bitmap = board_mask(w, h); // Clear last valid bit bitmap &= ~(1ULL << (cells - 1)); @@ -235,7 +235,7 @@ TEST_CASE("Board sizes between 33 and 63 cells", "[puzzle][midrange]") for (int index : {31, 32, cells - 2}) { if (index >= cells) continue; - uint64_t bitmap = board_mask(w, h); + u64 bitmap = board_mask(w, h); bitmap &= ~(1ULL << index); int x = -1, y = -1; @@ -250,7 +250,7 @@ TEST_CASE("Multiple holes choose lowest index", "[puzzle][multiple]") { puzzle p(5, 5); - uint64_t bitmap = board_mask(5, 5); + u64 bitmap = board_mask(5, 5); // Clear several positions bitmap &= ~(1ULL << 3); diff --git a/test/bits.cpp b/test/bits.cpp index 9a63b55..de62efd 100644 --- a/test/bits.cpp +++ b/test/bits.cpp @@ -31,8 +31,8 @@ // 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 +// This avoids duplicating identical logic for u8, u16, u32, +// and u64. 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]") @@ -49,7 +49,7 @@ // --------------------------------------------------------------------------- TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]", - uint8_t, uint16_t, uint32_t, uint64_t) + u8, u16, u32, u64) { SECTION("single bit mask at bit 0") { auto m = create_mask(0, 0); @@ -72,16 +72,16 @@ TEMPLATE_TEST_CASE("create_mask produces correct masks", "[create_mask]", } SECTION("full-width mask returns all ones") { - constexpr uint8_t last = sizeof(TestType) * 8 - 1; + constexpr u8 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); + REQUIRE(create_mask(0, 15) == 0x0000FFFF); + REQUIRE(create_mask(0, 31) == 0xFFFFFFFF); + REQUIRE(create_mask(16, 31) == 0xFFFF0000); } // --------------------------------------------------------------------------- @@ -89,11 +89,11 @@ TEST_CASE("create_mask 32-bit specific cases", "[create_mask]") { // --------------------------------------------------------------------------- TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]", - uint8_t, uint16_t, uint32_t, uint64_t) + u8, u16, u32, u64) { SECTION("clear all bits") { TestType val = static_cast(~TestType{0}); - constexpr uint8_t last = sizeof(TestType) * 8 - 1; + constexpr u8 last = sizeof(TestType) * 8 - 1; clear_bits(val, 0, last); REQUIRE(val == TestType{0}); } @@ -128,55 +128,55 @@ TEMPLATE_TEST_CASE("clear_bits zeroes the specified range", "[clear_bits]", // --------------------------------------------------------------------------- TEMPLATE_TEST_CASE("set_bits writes value into the specified range", "[set_bits]", - uint8_t, uint16_t, uint32_t, uint64_t) + u8, u16, u32, u64) { SECTION("set lower nibble on zero") { TestType val = TestType{0}; - set_bits(val, uint8_t{0}, uint8_t{3}, static_cast(0xA)); + set_bits(val, u8{0}, u8{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)); + set_bits(val, u8{4}, u8{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)); + set_bits(val, u8{0}, u8{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)); + set_bits(val, u8{3}, u8{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)); + set_bits(val, u8{3}, u8{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)); + set_bits(val, u8{0}, u8{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)); + u32 val = 0; + constexpr u8 small_val = 0x3F; + set_bits(val, u8{8}, u8{13}, small_val); + REQUIRE(val == (u32{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}); + u32 val = 0xDEADBEEF; + set_bits(val, u8{8}, u8{15}, u32{0x42}); REQUIRE(val == 0xDEAD42EF); } @@ -185,53 +185,53 @@ TEST_CASE("set_bits preserves surrounding bits in 32-bit", "[set_bits]") { // --------------------------------------------------------------------------- TEMPLATE_TEST_CASE("get_bits extracts the specified range", "[get_bits]", - uint8_t, uint16_t, uint32_t, uint64_t) + u8, u16, u32, u64) { SECTION("get lower nibble") { TestType val = static_cast(0xAB); - auto result = get_bits(val, uint8_t{0}, uint8_t{3}); + auto result = get_bits(val, u8{0}, u8{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}); + auto result = get_bits(val, u8{4}, u8{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}); + auto result = get_bits(val, u8{3}, u8{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}); + auto result = get_bits(val, u8{3}, u8{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); + constexpr u8 last = sizeof(TestType) * 8 - 1; + auto result = get_bits(val, u8{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}); + auto result = get_bits(val, u8{0}, u8{7}); REQUIRE(result == TestType{0}); } } TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") { - constexpr uint32_t val = 0xDEADBEEF; + constexpr u32 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); + REQUIRE(get_bits(val, u8{0}, u8{7}) == 0xEF); + REQUIRE(get_bits(val, u8{8}, u8{15}) == 0xBE); + REQUIRE(get_bits(val, u8{16}, u8{23}) == 0xAD); + REQUIRE(get_bits(val, u8{24}, u8{31}) == 0xDE); } // --------------------------------------------------------------------------- @@ -239,29 +239,29 @@ TEST_CASE("get_bits 32-bit specific extractions", "[get_bits]") { // --------------------------------------------------------------------------- TEST_CASE("set_bits then get_bits round-trips correctly", "[round-trip]") { - uint32_t reg = 0; + u32 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); + set_bits(reg, u8{4}, u8{11}, u32{0xAB}); + REQUIRE(get_bits(reg, u8{4}, u8{11}) == 0xAB); - REQUIRE(get_bits(reg, uint8_t{0}, uint8_t{3}) == 0x0); - REQUIRE(get_bits(reg, uint8_t{12}, uint8_t{31}) == 0x0); + REQUIRE(get_bits(reg, u8{0}, u8{3}) == 0x0); + REQUIRE(get_bits(reg, u8{12}, u8{31}) == 0x0); } TEST_CASE("multiple set_bits on different ranges", "[round-trip]") { - uint32_t reg = 0; + u32 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}); + set_bits(reg, u8{0}, u8{7}, u32{0x01}); + set_bits(reg, u8{8}, u8{15}, u32{0x02}); + set_bits(reg, u8{16}, u8{23}, u32{0x03}); + set_bits(reg, u8{24}, u8{31}, u32{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}); + u64 reg = 0; + set_bits(reg, u8{32}, u8{63}, u64{0xCAFEBABE}); + REQUIRE(get_bits(reg, u8{32}, u8{63}) == u64{0xCAFEBABE}); + REQUIRE(get_bits(reg, u8{0}, u8{31}) == u64{0}); } \ No newline at end of file diff --git a/test/puzzle.cpp b/test/puzzle.cpp index 5093a8c..8b19055 100644 --- a/test/puzzle.cpp +++ b/test/puzzle.cpp @@ -12,7 +12,7 @@ TEST_CASE("Block creation and field access", "[block]") { - puzzle::block b(1, 2, 3, 4, true, false); + block b(1, 2, 3, 4, true, false); CHECK(b.get_x() == 1); CHECK(b.get_y() == 2); @@ -25,16 +25,16 @@ TEST_CASE("Block creation and field access", "[block]") TEST_CASE("Block invalid by default", "[block]") { - puzzle::block b; + block b; CHECK_FALSE(b.valid()); } TEST_CASE("Block comparison ordering", "[block]") { // Row-major: (0,0) < (1,0) < (0,1) - puzzle::block a(0, 0, 1, 1); - puzzle::block b(1, 0, 1, 1); - puzzle::block c(0, 1, 1, 1); + block a(0, 0, 1, 1); + block b(1, 0, 1, 1); + block c(0, 1, 1, 1); CHECK(a < b); CHECK(b < c); @@ -43,7 +43,7 @@ TEST_CASE("Block comparison ordering", "[block]") TEST_CASE("Block principal_dirs for horizontal block", "[block]") { - puzzle::block b(0, 0, 3, 1); // wider than tall + block b(0, 0, 3, 1); // wider than tall CHECK((b.principal_dirs() & eas)); CHECK((b.principal_dirs() & wes)); CHECK_FALSE((b.principal_dirs() & nor)); @@ -52,7 +52,7 @@ TEST_CASE("Block principal_dirs for horizontal block", "[block]") TEST_CASE("Block principal_dirs for vertical block", "[block]") { - puzzle::block b(0, 0, 1, 3); // taller than wide + block b(0, 0, 1, 3); // taller than wide CHECK((b.principal_dirs() & nor)); CHECK((b.principal_dirs() & sou)); CHECK_FALSE((b.principal_dirs() & eas)); @@ -61,7 +61,7 @@ TEST_CASE("Block principal_dirs for vertical block", "[block]") TEST_CASE("Block principal_dirs for square block", "[block]") { - puzzle::block b(0, 0, 2, 2); + block b(0, 0, 2, 2); CHECK((b.principal_dirs() & nor)); CHECK((b.principal_dirs() & sou)); CHECK((b.principal_dirs() & eas)); @@ -70,7 +70,7 @@ TEST_CASE("Block principal_dirs for square block", "[block]") TEST_CASE("Block covers", "[block]") { - puzzle::block b(1, 2, 3, 2); + block b(1, 2, 3, 2); // Covers (1,2) to (3,3) CHECK(b.covers(1, 2)); CHECK(b.covers(3, 3)); @@ -83,10 +83,10 @@ TEST_CASE("Block covers", "[block]") TEST_CASE("Block collides", "[block]") { - puzzle::block a(0, 0, 2, 2); - puzzle::block b(1, 1, 2, 2); - puzzle::block c(2, 2, 1, 1); - puzzle::block d(3, 0, 1, 1); + block a(0, 0, 2, 2); + block b(1, 1, 2, 2); + block c(2, 2, 1, 1); + block d(3, 0, 1, 1); CHECK(a.collides(b)); CHECK(b.collides(a)); @@ -114,7 +114,7 @@ TEST_CASE("Puzzle creation and meta access", "[puzzle]") TEST_CASE("Puzzle add and remove block", "[puzzle]") { puzzle p(4, 4, 0, 0, false, false); - puzzle::block b(0, 0, 2, 1); + block b(0, 0, 2, 1); auto p2 = p.try_add_block(b); REQUIRE(p2.has_value()); @@ -127,8 +127,8 @@ TEST_CASE("Puzzle add and remove block", "[puzzle]") TEST_CASE("Puzzle meta roundtrip for all valid sizes", "[puzzle]") { - for (uint8_t w = 3; w <= 8; ++w) { - for (uint8_t h = 3; h <= 8; ++h) { + for (u8 w = 3; w <= 8; ++w) { + for (u8 h = 3; h <= 8; ++h) { puzzle p(w, h, 0, 0, true, false); CHECK(p.get_width() == w); CHECK(p.get_height() == h); @@ -151,11 +151,11 @@ TEST_CASE("Puzzle block_count", "[puzzle]") puzzle p(4, 4, 0, 0, false, false); CHECK(p.block_count() == 0); - auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1)); + auto p2 = p.try_add_block(block(0, 0, 1, 1)); REQUIRE(p2); CHECK(p2->block_count() == 1); - auto p3 = p2->try_add_block(puzzle::block(2, 2, 1, 1)); + auto p3 = p2->try_add_block(block(2, 2, 1, 1)); REQUIRE(p3); CHECK(p3->block_count() == 2); } @@ -163,18 +163,18 @@ TEST_CASE("Puzzle block_count", "[puzzle]") TEST_CASE("Puzzle cannot add overlapping block", "[puzzle]") { puzzle p(4, 4, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 2)); + auto p2 = p.try_add_block(block(0, 0, 2, 2)); REQUIRE(p2); // Overlapping - auto p3 = p2->try_add_block(puzzle::block(1, 1, 2, 2)); + auto p3 = p2->try_add_block(block(1, 1, 2, 2)); CHECK_FALSE(p3.has_value()); } TEST_CASE("Puzzle cannot add block outside board", "[puzzle]") { puzzle p(4, 4, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(3, 3, 2, 2)); + auto p2 = p.try_add_block(block(3, 3, 2, 2)); CHECK_FALSE(p2.has_value()); } @@ -184,17 +184,17 @@ TEST_CASE("Puzzle cannot add block outside board", "[puzzle]") TEST_CASE("bitmap_set_bit and bitmap_get_bit roundtrip", "[bitmap]") { - for (uint8_t w = 3; w <= 8; ++w) { - uint64_t bm = 0; + for (u8 w = 3; w <= 8; ++w) { + u64 bm = 0; // Set every cell on a w x w board - for (uint8_t y = 0; y < w; ++y) { - for (uint8_t x = 0; x < w; ++x) { + for (u8 y = 0; y < w; ++y) { + for (u8 x = 0; x < w; ++x) { puzzle::bitmap_set_bit(bm, w, x, y); } } // Verify all set - for (uint8_t y = 0; y < w; ++y) { - for (uint8_t x = 0; x < w; ++x) { + for (u8 y = 0; y < w; ++y) { + for (u8 x = 0; x < w; ++x) { CHECK(puzzle::bitmap_get_bit(bm, w, x, y)); } } @@ -203,12 +203,12 @@ TEST_CASE("bitmap_set_bit and bitmap_get_bit roundtrip", "[bitmap]") TEST_CASE("bitmap_set_bit sets only the target bit", "[bitmap]") { - for (uint8_t w = 3; w <= 8; ++w) { - for (uint8_t y = 0; y < w; ++y) { - for (uint8_t x = 0; x < w; ++x) { - uint64_t bm = 0; + for (u8 w = 3; w <= 8; ++w) { + for (u8 y = 0; y < w; ++y) { + for (u8 x = 0; x < w; ++x) { + u64 bm = 0; puzzle::bitmap_set_bit(bm, w, x, y); - CHECK(bm == (uint64_t(1) << (y * w + x))); + CHECK(bm == (u64(1) << (y * w + x))); } } } @@ -216,11 +216,11 @@ TEST_CASE("bitmap_set_bit sets only the target bit", "[bitmap]") TEST_CASE("bitmap_clear_bit clears only the target bit", "[bitmap]") { - uint8_t w = 6; - uint64_t bm = 0; + u8 w = 6; + u64 bm = 0; puzzle::bitmap_set_bit(bm, w, 2, 3); puzzle::bitmap_set_bit(bm, w, 4, 1); - uint64_t before = bm; + u64 before = bm; puzzle::bitmap_clear_bit(bm, w, 2, 3); CHECK_FALSE(puzzle::bitmap_get_bit(bm, w, 2, 3)); @@ -230,10 +230,10 @@ TEST_CASE("bitmap_clear_bit clears only the target bit", "[bitmap]") TEST_CASE("blocks_bitmap for single block", "[bitmap]") { puzzle p(6, 6, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(1, 2, 3, 1)); + auto p2 = p.try_add_block(block(1, 2, 3, 1)); REQUIRE(p2); - uint64_t bm = p2->blocks_bitmap(); + u64 bm = p2->blocks_bitmap(); // Block at (1,2) width 3 height 1 on a 6-wide board // Bits: row 2, cols 1,2,3 -> positions 2*6+1=13, 14, 15 CHECK(puzzle::bitmap_get_bit(bm, 6, 1, 2)); @@ -248,10 +248,10 @@ TEST_CASE("blocks_bitmap for single block", "[bitmap]") TEST_CASE("blocks_bitmap for 2x2 block", "[bitmap]") { puzzle p(5, 5, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 2)); + auto p2 = p.try_add_block(block(1, 1, 2, 2)); REQUIRE(p2); - uint64_t bm = p2->blocks_bitmap(); + u64 bm = p2->blocks_bitmap(); CHECK(puzzle::bitmap_get_bit(bm, 5, 1, 1)); CHECK(puzzle::bitmap_get_bit(bm, 5, 2, 1)); CHECK(puzzle::bitmap_get_bit(bm, 5, 1, 2)); @@ -264,13 +264,13 @@ TEST_CASE("blocks_bitmap_h only includes horizontal/square blocks", "[bitmap]") { puzzle p(6, 6, 0, 0, true, false); // Horizontal block (2x1) -> principal_dirs has eas - auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1)); + auto p2 = p.try_add_block(block(0, 0, 2, 1)); REQUIRE(p2); // Vertical block (1x2) -> principal_dirs has sou, not eas - auto p3 = p2->try_add_block(puzzle::block(4, 0, 1, 2)); + auto p3 = p2->try_add_block(block(4, 0, 1, 2)); REQUIRE(p3); - uint64_t bm_h = p3->blocks_bitmap_h(); + u64 bm_h = p3->blocks_bitmap_h(); // Horizontal block should be in bitmap_h CHECK(puzzle::bitmap_get_bit(bm_h, 6, 0, 0)); CHECK(puzzle::bitmap_get_bit(bm_h, 6, 1, 0)); @@ -283,13 +283,13 @@ TEST_CASE("blocks_bitmap_v only includes vertical/square blocks", "[bitmap]") { puzzle p(6, 6, 0, 0, true, false); // Vertical block (1x2) - auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 2)); + auto p2 = p.try_add_block(block(0, 0, 1, 2)); REQUIRE(p2); // Horizontal block (2x1) - auto p3 = p2->try_add_block(puzzle::block(4, 0, 2, 1)); + auto p3 = p2->try_add_block(block(4, 0, 2, 1)); REQUIRE(p3); - uint64_t bm_v = p3->blocks_bitmap_v(); + u64 bm_v = p3->blocks_bitmap_v(); // Vertical block should be in bitmap_v CHECK(puzzle::bitmap_get_bit(bm_v, 6, 0, 0)); CHECK(puzzle::bitmap_get_bit(bm_v, 6, 0, 1)); @@ -301,11 +301,11 @@ TEST_CASE("blocks_bitmap_v only includes vertical/square blocks", "[bitmap]") TEST_CASE("blocks_bitmap_h and blocks_bitmap_v both include square blocks", "[bitmap]") { puzzle p(6, 6, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 2)); + auto p2 = p.try_add_block(block(1, 1, 2, 2)); REQUIRE(p2); - uint64_t bm_h = p2->blocks_bitmap_h(); - uint64_t bm_v = p2->blocks_bitmap_v(); + u64 bm_h = p2->blocks_bitmap_h(); + u64 bm_v = p2->blocks_bitmap_v(); // Square block should appear in both CHECK(puzzle::bitmap_get_bit(bm_h, 6, 1, 1)); @@ -320,12 +320,12 @@ TEST_CASE("bitmap_newly_occupied north - single vertical block with space above" { // 4x4 board, vertical 1x2 block at (1,2) puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2)); + auto p2 = p.try_add_block(block(1, 2, 1, 2)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t vert = p2->blocks_bitmap_v(); - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 vert = p2->blocks_bitmap_v(); + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, vert, nor); @@ -346,12 +346,12 @@ TEST_CASE("bitmap_newly_occupied south - single vertical block with space below" { // 4x4 board, vertical 1x2 block at (1,0) puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2)); + auto p2 = p.try_add_block(block(1, 0, 1, 2)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t vert = p2->blocks_bitmap_v(); - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 vert = p2->blocks_bitmap_v(); + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, vert, sou); @@ -366,12 +366,12 @@ TEST_CASE("bitmap_newly_occupied east - single horizontal block with space right { // 5x4 board, horizontal 2x1 block at (0,1) puzzle p(5, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1)); + auto p2 = p.try_add_block(block(0, 1, 2, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, horiz, eas); @@ -386,12 +386,12 @@ TEST_CASE("bitmap_newly_occupied west - single horizontal block with space left" { // 5x4 board, horizontal 2x1 block at (2,1) puzzle p(5, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(2, 1, 2, 1)); + auto p2 = p.try_add_block(block(2, 1, 2, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, horiz, wes); @@ -406,12 +406,12 @@ TEST_CASE("bitmap_newly_occupied does not wrap east across rows", "[bitmap_move] { // 4x4 board, horizontal 2x1 block at (2,0) - rightmost position puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(2, 0, 2, 1)); + auto p2 = p.try_add_block(block(2, 0, 2, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, horiz, eas); @@ -424,12 +424,12 @@ TEST_CASE("bitmap_newly_occupied does not wrap west across rows", "[bitmap_move] { // 4x4 board, horizontal 2x1 block at (0,1) puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1)); + auto p2 = p.try_add_block(block(0, 1, 2, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, horiz, wes); @@ -441,14 +441,14 @@ TEST_CASE("bitmap_newly_occupied blocked by another block", "[bitmap_move]") { // 6x4 board, two horizontal blocks side by side puzzle p(6, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1)); + auto p2 = p.try_add_block(block(0, 0, 2, 1)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(2, 0, 2, 1)); + auto p3 = p2->try_add_block(block(2, 0, 2, 1)); REQUIRE(p3); - uint64_t combined = p3->blocks_bitmap(); - uint64_t horiz = p3->blocks_bitmap_h(); - uint64_t bm = combined; + u64 combined = p3->blocks_bitmap(); + u64 horiz = p3->blocks_bitmap_h(); + u64 bm = combined; p3->bitmap_newly_occupied_after_move(bm, horiz, eas); @@ -468,11 +468,11 @@ TEST_CASE("restricted_bitmap_get_moves north - returns correct source cell", "[g { // 4x4 board, vertical 1x2 block at (1,2) puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2)); + auto p2 = p.try_add_block(block(1, 2, 1, 2)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t vert = p2->blocks_bitmap_v(); + u64 combined = p2->blocks_bitmap(); + u64 vert = p2->blocks_bitmap_v(); auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor); @@ -487,11 +487,11 @@ TEST_CASE("restricted_bitmap_get_moves south - returns correct source cell", "[g { // 4x4 board, vertical 1x2 block at (1,0) puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2)); + auto p2 = p.try_add_block(block(1, 0, 1, 2)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t vert = p2->blocks_bitmap_v(); + u64 combined = p2->blocks_bitmap(); + u64 vert = p2->blocks_bitmap_v(); auto moves = p2->restricted_bitmap_get_moves(combined, vert, sou); @@ -511,11 +511,11 @@ TEST_CASE("restricted_bitmap_get_moves east - returns correct source cell", "[ge { // 5x4 board, horizontal 2x1 block at (0,1) puzzle p(5, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1)); + auto p2 = p.try_add_block(block(0, 1, 2, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); auto moves = p2->restricted_bitmap_get_moves(combined, horiz, eas); @@ -530,11 +530,11 @@ TEST_CASE("restricted_bitmap_get_moves west - returns correct source cell", "[ge { // 5x4 board, horizontal 2x1 block at (2,1) puzzle p(5, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(2, 1, 2, 1)); + auto p2 = p.try_add_block(block(2, 1, 2, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); auto moves = p2->restricted_bitmap_get_moves(combined, horiz, wes); @@ -549,11 +549,11 @@ TEST_CASE("restricted_bitmap_get_moves returns empty when blocked", "[get_moves] { // 4x4 board, vertical 1x2 block at (1,0) - can't move north (at top edge) puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2)); + auto p2 = p.try_add_block(block(1, 0, 1, 2)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t vert = p2->blocks_bitmap_v(); + u64 combined = p2->blocks_bitmap(); + u64 vert = p2->blocks_bitmap_v(); auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor); @@ -569,11 +569,11 @@ TEST_CASE("restricted_bitmap_get_moves with width 6 board", "[get_moves]") { // 6x6 board, vertical 1x2 block at (2,3) puzzle p(6, 6, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(2, 3, 1, 2)); + auto p2 = p.try_add_block(block(2, 3, 1, 2)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t vert = p2->blocks_bitmap_v(); + u64 combined = p2->blocks_bitmap(); + u64 vert = p2->blocks_bitmap_v(); auto moves = p2->restricted_bitmap_get_moves(combined, vert, nor); @@ -588,20 +588,20 @@ TEST_CASE("restricted_bitmap_get_moves multiple blocks can move", "[get_moves]") { // 6x6 board, two vertical blocks with space above puzzle p(6, 6, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 2, 1, 2)); + auto p2 = p.try_add_block(block(1, 2, 1, 2)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(4, 3, 1, 2)); + auto p3 = p2->try_add_block(block(4, 3, 1, 2)); REQUIRE(p3); - uint64_t combined = p3->blocks_bitmap(); - uint64_t vert = p3->blocks_bitmap_v(); + u64 combined = p3->blocks_bitmap(); + u64 vert = p3->blocks_bitmap_v(); auto moves = p3->restricted_bitmap_get_moves(combined, vert, nor); // Both blocks can move north // Block 1: newly occupied (1,1), source (1,2) = 1+2*6 = 13 // Block 2: newly occupied (4,2), source (4,3) = 4+3*6 = 22 - std::set move_set; + std::set move_set; for (int i = 0; i < puzzle::MAX_BLOCKS && moves[i] != 0xFF; ++i) { move_set.insert(moves[i]); } @@ -617,7 +617,7 @@ TEST_CASE("restricted_bitmap_get_moves multiple blocks can move", "[get_moves]") TEST_CASE("try_move_block_at basic moves", "[move]") { puzzle p(4, 4, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1)); + auto p2 = p.try_add_block(block(1, 1, 1, 1)); REQUIRE(p2); auto north = p2->try_move_block_at(1, 1, nor); @@ -640,7 +640,7 @@ TEST_CASE("try_move_block_at basic moves", "[move]") TEST_CASE("try_move_block_at blocked by edge", "[move]") { puzzle p(4, 4, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1)); + auto p2 = p.try_add_block(block(0, 0, 1, 1)); REQUIRE(p2); CHECK_FALSE(p2->try_move_block_at(0, 0, nor).has_value()); @@ -650,9 +650,9 @@ TEST_CASE("try_move_block_at blocked by edge", "[move]") TEST_CASE("try_move_block_at blocked by collision", "[move]") { puzzle p(4, 4, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1)); + auto p2 = p.try_add_block(block(0, 0, 1, 1)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(1, 0, 1, 1)); + auto p3 = p2->try_add_block(block(1, 0, 1, 1)); REQUIRE(p3); CHECK_FALSE(p3->try_move_block_at(0, 0, eas).has_value()); @@ -661,12 +661,12 @@ TEST_CASE("try_move_block_at blocked by collision", "[move]") TEST_CASE("try_move_block_at_fast matches try_move_block_at", "[move]") { puzzle p(5, 5, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 1)); + auto p2 = p.try_add_block(block(1, 1, 2, 1)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(0, 3, 1, 2)); + auto p3 = p2->try_add_block(block(0, 3, 1, 2)); REQUIRE(p3); - uint64_t bm = p3->blocks_bitmap(); + u64 bm = p3->blocks_bitmap(); for (const dir d : {nor, eas, sou, wes}) { // Block 0 is the first in sorted order @@ -686,7 +686,7 @@ TEST_CASE("try_move_block_at restricted mode respects principal dirs", "[move]") { puzzle p(5, 5, 0, 0, true, false); // restricted // Horizontal block 2x1 at (1,2) - auto p2 = p.try_add_block(puzzle::block(1, 2, 2, 1)); + auto p2 = p.try_add_block(block(1, 2, 2, 1)); REQUIRE(p2); // In restricted mode, horizontal block can only move east/west @@ -760,16 +760,16 @@ static auto collect_adjacent_simple(const puzzle& p) -> std::set // Helper: collect all adjacent states using for_each_adjacent_restricted static auto collect_adjacent_restricted(const puzzle& p) -> std::set { - const uint8_t w = p.get_width(); - std::array bitmap_block_indices{}; + const u8 w = p.get_width(); + std::array bitmap_block_indices{}; for (size_t i = 0; i < puzzle::MAX_BLOCKS; ++i) { - const puzzle::block b(p.repr_view().data()[i]); + const block b(p.repr_view().data()[i]); if (i >= static_cast(p.block_count())) { break; } const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); - for (uint8_t x = bx; x < bx + bw; ++x) { - for (uint8_t y = by; y < by + bh; ++y) { + for (u8 x = bx; x < bx + bw; ++x) { + for (u8 y = by; y < by + bh; ++y) { bitmap_block_indices[y * w + x] = i; } } @@ -786,7 +786,7 @@ static auto collect_adjacent_restricted(const puzzle& p) -> std::set TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - single block", "[cross_validate]") { puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1)); + auto p2 = p.try_add_block(block(1, 1, 1, 1)); REQUIRE(p2); auto simple = collect_adjacent_simple(*p2); @@ -798,7 +798,7 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - single block TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - horizontal block", "[cross_validate]") { puzzle p(5, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 2, 1)); + auto p2 = p.try_add_block(block(1, 1, 2, 1)); REQUIRE(p2); auto simple = collect_adjacent_simple(*p2); @@ -810,7 +810,7 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - horizontal b TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - vertical block", "[cross_validate]") { puzzle p(4, 5, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 2)); + auto p2 = p.try_add_block(block(1, 1, 1, 2)); REQUIRE(p2); auto simple = collect_adjacent_simple(*p2); @@ -822,9 +822,9 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - vertical blo TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - two blocks", "[cross_validate]") { puzzle p(5, 5, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1)); // horizontal + auto p2 = p.try_add_block(block(0, 0, 2, 1)); // horizontal REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(4, 1, 1, 2)); // vertical + auto p3 = p2->try_add_block(block(4, 1, 1, 2)); // vertical REQUIRE(p3); auto simple = collect_adjacent_simple(*p3); @@ -836,13 +836,13 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - two blocks", TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - crowded board", "[cross_validate]") { puzzle p(5, 5, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1)); + auto p2 = p.try_add_block(block(0, 0, 2, 1)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(3, 0, 1, 2)); + auto p3 = p2->try_add_block(block(3, 0, 1, 2)); REQUIRE(p3); - auto p4 = p3->try_add_block(puzzle::block(0, 2, 1, 3)); + auto p4 = p3->try_add_block(block(0, 2, 1, 3)); REQUIRE(p4); - auto p5 = p4->try_add_block(puzzle::block(2, 3, 2, 1)); + auto p5 = p4->try_add_block(block(2, 3, 2, 1)); REQUIRE(p5); auto simple = collect_adjacent_simple(*p5); @@ -854,13 +854,13 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - crowded boar TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - 6x6 board", "[cross_validate]") { puzzle p(6, 6, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 3, 1)); // horizontal + auto p2 = p.try_add_block(block(0, 0, 3, 1)); // horizontal REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(5, 0, 1, 2)); // vertical + auto p3 = p2->try_add_block(block(5, 0, 1, 2)); // vertical REQUIRE(p3); - auto p4 = p3->try_add_block(puzzle::block(2, 2, 1, 2)); // vertical + auto p4 = p3->try_add_block(block(2, 2, 1, 2)); // vertical REQUIRE(p4); - auto p5 = p4->try_add_block(puzzle::block(3, 2, 2, 1)); // horizontal + auto p5 = p4->try_add_block(block(3, 2, 2, 1)); // horizontal REQUIRE(p5); auto simple = collect_adjacent_simple(*p5); @@ -889,7 +889,7 @@ TEST_CASE("for_each_adjacent_restricted matches for_each_adjacent - the main puz TEST_CASE("explore_state_space simple case - single 1x1 block", "[explore]") { puzzle p(3, 3, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(1, 1, 1, 1)); + auto p2 = p.try_add_block(block(1, 1, 1, 1)); REQUIRE(p2); auto [states, transitions] = p2->explore_state_space(); @@ -901,7 +901,7 @@ TEST_CASE("explore_state_space simple case - single 1x1 block", "[explore]") TEST_CASE("explore_state_space restricted - vertical block", "[explore]") { puzzle p(3, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(1, 0, 1, 2)); + auto p2 = p.try_add_block(block(1, 0, 1, 2)); REQUIRE(p2); auto [states, transitions] = p2->explore_state_space(); @@ -914,7 +914,7 @@ TEST_CASE("explore_state_space restricted - vertical block", "[explore]") TEST_CASE("explore_state_space restricted - horizontal block", "[explore]") { puzzle p(4, 3, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 1, 2, 1)); + auto p2 = p.try_add_block(block(0, 1, 2, 1)); REQUIRE(p2); auto [states, transitions] = p2->explore_state_space(); @@ -927,7 +927,7 @@ TEST_CASE("explore_state_space restricted - horizontal block", "[explore]") TEST_CASE("explore_state_space restricted - square block", "[explore]") { puzzle p(3, 3, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1)); + auto p2 = p.try_add_block(block(0, 0, 1, 1)); REQUIRE(p2); auto [states, transitions] = p2->explore_state_space(); @@ -939,7 +939,7 @@ TEST_CASE("explore_state_space restricted - square block", "[explore]") TEST_CASE("explore_state_space preserves board metadata", "[explore]") { puzzle p(5, 4, 2, 1, true, true); - auto p2 = p.try_add_block(puzzle::block(0, 0, 1, 1, true)); + auto p2 = p.try_add_block(block(0, 0, 1, 1, true)); REQUIRE(p2); auto [states, transitions] = p2->explore_state_space(); @@ -959,9 +959,9 @@ TEST_CASE("explore_state_space two blocks restricted", "[explore]") // 4x4 board, restricted // Horizontal 2x1 at (0,0) and vertical 1x2 at (3,0) puzzle p(4, 4, 0, 0, true, false); - auto p2 = p.try_add_block(puzzle::block(0, 0, 2, 1)); + auto p2 = p.try_add_block(block(0, 0, 2, 1)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(3, 0, 1, 2)); + auto p3 = p2->try_add_block(block(3, 0, 1, 2)); REQUIRE(p3); auto [states, transitions] = p3->explore_state_space(); @@ -988,15 +988,15 @@ TEST_CASE("explore_state_space two blocks restricted", "[explore]") TEST_CASE("East move does not wrap for various board widths", "[bitmap_move]") { - for (uint8_t w = 3; w <= 8; ++w) { + for (u8 w = 3; w <= 8; ++w) { puzzle p(w, 3, 0, 0, true, false); // Place a 1x1 block at rightmost column, row 0 - auto p2 = p.try_add_block(puzzle::block(w - 1, 0, 1, 1)); + auto p2 = p.try_add_block(block(w - 1, 0, 1, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); // 1x1 is square, so in both h and v - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); // 1x1 is square, so in both h and v + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, horiz, eas); @@ -1009,15 +1009,15 @@ TEST_CASE("East move does not wrap for various board widths", "[bitmap_move]") TEST_CASE("West move does not wrap for various board widths", "[bitmap_move]") { - for (uint8_t w = 3; w <= 8; ++w) { + for (u8 w = 3; w <= 8; ++w) { puzzle p(w, 3, 0, 0, true, false); // Place a 1x1 block at leftmost column, row 1 - auto p2 = p.try_add_block(puzzle::block(0, 1, 1, 1)); + auto p2 = p.try_add_block(block(0, 1, 1, 1)); REQUIRE(p2); - uint64_t combined = p2->blocks_bitmap(); - uint64_t horiz = p2->blocks_bitmap_h(); - uint64_t bm = combined; + u64 combined = p2->blocks_bitmap(); + u64 horiz = p2->blocks_bitmap_h(); + u64 bm = combined; p2->bitmap_newly_occupied_after_move(bm, horiz, wes); @@ -1034,34 +1034,34 @@ TEST_CASE("West move does not wrap for various board widths", "[bitmap_move]") TEST_CASE("bitmap_check_collision detects collision", "[collision]") { puzzle p(5, 5, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(2, 2, 1, 1)); + auto p2 = p.try_add_block(block(2, 2, 1, 1)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(0, 0, 2, 1)); + auto p3 = p2->try_add_block(block(0, 0, 2, 1)); REQUIRE(p3); - uint64_t bm = p3->blocks_bitmap(); + u64 bm = p3->blocks_bitmap(); // Check if a hypothetical block at (1,2) 2x1 collides - puzzle::block hyp(1, 2, 2, 1); + block hyp(1, 2, 2, 1); CHECK(p3->bitmap_check_collision(bm, hyp)); // overlaps with (2,2) // Check if a hypothetical block at (3,3) 1x1 collides - puzzle::block hyp2(3, 3, 1, 1); + block hyp2(3, 3, 1, 1); CHECK_FALSE(p3->bitmap_check_collision(bm, hyp2)); } TEST_CASE("bitmap_check_collision directional", "[collision]") { puzzle p(5, 5, 0, 0, false, false); - auto p2 = p.try_add_block(puzzle::block(2, 0, 1, 1)); + auto p2 = p.try_add_block(block(2, 0, 1, 1)); REQUIRE(p2); - auto p3 = p2->try_add_block(puzzle::block(2, 1, 1, 1)); + auto p3 = p2->try_add_block(block(2, 1, 1, 1)); REQUIRE(p3); - uint64_t bm = p3->blocks_bitmap(); + u64 bm = p3->blocks_bitmap(); // Clear block at (2,1) from bitmap to check if (2,1) can move north - puzzle::block b(2, 1, 1, 1); + block b(2, 1, 1, 1); p3->bitmap_clear_block(bm, b); // (2,1) moving north -> check (2,0) which has a block